@pulse-compute/cli 0.0.0 → 1.0.0-beta.2
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/API.md +572 -0
- package/CHANGELOG.md +71 -0
- package/README.md +71 -1
- package/bin/pulse.js +15 -0
- package/bin/pulsewasm-extract.js +22 -0
- package/cli-spec.json +575 -0
- package/completions/_pulse +106 -0
- package/completions/pulse.bash +31 -0
- package/completions/pulse.fish +76 -0
- package/docs/README.md +77 -0
- package/docs/architecture/current-contracts.md +470 -0
- package/docs/architecture/overview.md +93 -0
- package/docs/architecture/vision.md +515 -0
- package/docs/concepts/bodies.md +186 -0
- package/docs/concepts/compilation-and-lowering.md +496 -0
- package/docs/concepts/contracts-and-providers.md +158 -0
- package/docs/concepts/effects-and-continuations.md +300 -0
- package/docs/concepts/entities-and-adapters.md +71 -0
- package/docs/concepts/package-owned-lowering.md +175 -0
- package/docs/concepts/targets-and-hosts.md +70 -0
- package/docs/contributing/README.md +38 -0
- package/docs/contributing/adding-core-provider.md +315 -0
- package/docs/contributing/adding-first-party-lowerer.md +327 -0
- package/docs/contributing/entities-lowering.md +91 -0
- package/docs/contributing/package-lowerer-contract.md +383 -0
- package/docs/contributing/pulse-aware-packages.md +149 -0
- package/docs/examples.md +112 -0
- package/docs/fixtures/inspect-fetch-composition.selected.json +181 -0
- package/docs/getting-started.md +128 -0
- package/docs/guides/compatibility-imports.md +60 -0
- package/docs/guides/deploying-fastly.md +188 -0
- package/docs/guides/deploying-node.md +143 -0
- package/docs/guides/events.md +233 -0
- package/docs/guides/fastly-capabilities.md +153 -0
- package/docs/guides/fetching-and-composition.md +101 -0
- package/docs/guides/grip.md +94 -0
- package/docs/guides/json-schemas.md +233 -0
- package/docs/guides/migrating-from-express.md +192 -0
- package/docs/guides/project-lifecycle.md +143 -0
- package/docs/guides/routing.md +202 -0
- package/docs/guides/troubleshooting.md +47 -0
- package/docs/maintainers/README.md +60 -0
- package/docs/maintainers/codex-maintainer.md +71 -0
- package/docs/maintainers/documentation-deployment.md +147 -0
- package/docs/maintainers/documentation-system.md +58 -0
- package/docs/maintainers/documentation-versioning.md +65 -0
- package/docs/maintainers/maintainer-charter.md +100 -0
- package/docs/maintainers/maintenance-policy.json +809 -0
- package/docs/maintainers/maintenance-policy.md +124 -0
- package/docs/maintainers/npm-publishing.md +154 -0
- package/docs/maintainers/plugin-readiness.json +19 -0
- package/docs/maintainers/plugin-readiness.md +31 -0
- package/docs/maintainers/public-site.md +158 -0
- package/docs/maintainers/release-acceptance.md +293 -0
- package/docs/maintainers/release-manifest.md +89 -0
- package/docs/maintainers/repository-setup.md +145 -0
- package/docs/maintainers/scope-policy.md +105 -0
- package/docs/maintainers/support-and-triage.md +74 -0
- package/docs/maintainers/testing.md +334 -0
- package/docs/packages/README.md +60 -0
- package/docs/packages/assets.md +85 -0
- package/docs/packages/cli.md +190 -0
- package/docs/packages/crypto.md +78 -0
- package/docs/packages/entities.md +129 -0
- package/docs/packages/grip.md +82 -0
- package/docs/packages/implementation-packages.md +122 -0
- package/docs/packages/jwt.md +55 -0
- package/docs/packages/provider-fastly.md +243 -0
- package/docs/packages/pulse.md +159 -0
- package/docs/packages/runtime.md +265 -0
- package/docs/packages/s3.md +101 -0
- package/docs/preview-scope.md +142 -0
- package/docs/reference/README.md +33 -0
- package/docs/reference/cli-spec.json +575 -0
- package/docs/reference/cli.md +457 -0
- package/docs/reference/compatibility-matrix.md +169 -0
- package/docs/reference/diagnostics.md +1872 -0
- package/docs/reference/documentation-versions.json +22 -0
- package/docs/reference/environment.md +347 -0
- package/docs/reference/handler-authoring.md +264 -0
- package/docs/reference/project-config.md +582 -0
- package/docs/reference/project-config.schema.json +981 -0
- package/docs/reference/release-manifest.json +441 -0
- package/docs/reference/shell-completion.md +23 -0
- package/documentation-site.json +668 -0
- package/documentation-versions.json +22 -0
- package/examples/01-hello-json/.pulse/.gitignore +3 -0
- package/examples/01-hello-json/.pulse/config.ts +16 -0
- package/examples/01-hello-json/README.md +119 -0
- package/examples/01-hello-json/package.json +20 -0
- package/examples/01-hello-json/src/index.ts +9 -0
- package/examples/01-hello-json/tests/pulse.harness.ts +22 -0
- package/examples/01-hello-json/tsconfig.json +14 -0
- package/examples/02-request-schema/.pulse/.gitignore +3 -0
- package/examples/02-request-schema/.pulse/config.ts +17 -0
- package/examples/02-request-schema/README.md +145 -0
- package/examples/02-request-schema/package.json +20 -0
- package/examples/02-request-schema/src/index.ts +18 -0
- package/examples/02-request-schema/src/schemas.ts +20 -0
- package/examples/02-request-schema/tests/pulse.harness.ts +37 -0
- package/examples/02-request-schema/tsconfig.json +14 -0
- package/examples/03-fetch-composition/.pulse/.gitignore +3 -0
- package/examples/03-fetch-composition/.pulse/config.ts +31 -0
- package/examples/03-fetch-composition/README.md +210 -0
- package/examples/03-fetch-composition/package.json +20 -0
- package/examples/03-fetch-composition/src/index.ts +60 -0
- package/examples/03-fetch-composition/tests/pulse.harness.ts +47 -0
- package/examples/03-fetch-composition/tsconfig.json +14 -0
- package/examples/05-fastly-capabilities/.pulse/.gitignore +3 -0
- package/examples/05-fastly-capabilities/.pulse/config.ts +57 -0
- package/examples/05-fastly-capabilities/README.md +257 -0
- package/examples/05-fastly-capabilities/package.json +22 -0
- package/examples/05-fastly-capabilities/src/index.ts +55 -0
- package/examples/05-fastly-capabilities/tests/pulse.harness.ts +66 -0
- package/examples/05-fastly-capabilities/tsconfig.json +14 -0
- package/examples/07-opaque-proxy/.pulse/.gitignore +3 -0
- package/examples/07-opaque-proxy/.pulse/config.ts +38 -0
- package/examples/07-opaque-proxy/README.md +150 -0
- package/examples/07-opaque-proxy/package.json +21 -0
- package/examples/07-opaque-proxy/src/index.ts +9 -0
- package/examples/07-opaque-proxy/tests/pulse.harness.ts +25 -0
- package/examples/07-opaque-proxy/tsconfig.json +14 -0
- package/examples/09-router-lowering/.pulse/.gitignore +3 -0
- package/examples/09-router-lowering/.pulse/config.ts +22 -0
- package/examples/09-router-lowering/README.md +287 -0
- package/examples/09-router-lowering/package.json +22 -0
- package/examples/09-router-lowering/src/index.ts +65 -0
- package/examples/09-router-lowering/tests/pulse.harness.ts +91 -0
- package/examples/09-router-lowering/tsconfig.json +14 -0
- package/examples/10-entities-tools/.pulse/.gitignore +4 -0
- package/examples/10-entities-tools/.pulse/config.ts +23 -0
- package/examples/10-entities-tools/README.md +165 -0
- package/examples/10-entities-tools/package.json +21 -0
- package/examples/10-entities-tools/src/handlers.ts +15 -0
- package/examples/10-entities-tools/src/index.ts +31 -0
- package/examples/10-entities-tools/src/schemas.ts +18 -0
- package/examples/10-entities-tools/tests/pulse.harness.ts +40 -0
- package/examples/10-entities-tools/tools-facade.cjs +158 -0
- package/examples/10-entities-tools/tsconfig.json +14 -0
- package/examples/11-events/.pulse/.gitignore +4 -0
- package/examples/11-events/.pulse/config.ts +24 -0
- package/examples/11-events/README.md +194 -0
- package/examples/11-events/package.json +20 -0
- package/examples/11-events/src/index.ts +27 -0
- package/examples/11-events/src/schemas.ts +19 -0
- package/examples/11-events/tests/pulse.harness.ts +34 -0
- package/examples/11-events/tsconfig.json +15 -0
- package/examples/12-mcp-proxy/.pulse/.gitignore +3 -0
- package/examples/12-mcp-proxy/.pulse/config.ts +25 -0
- package/examples/12-mcp-proxy/README.md +149 -0
- package/examples/12-mcp-proxy/package.json +20 -0
- package/examples/12-mcp-proxy/src/index.ts +17 -0
- package/examples/12-mcp-proxy/tests/pulse.harness.ts +29 -0
- package/examples/12-mcp-proxy/tsconfig.json +14 -0
- package/examples/13-jwt-es256/.pulse/config.ts +19 -0
- package/examples/13-jwt-es256/README.md +195 -0
- package/examples/13-jwt-es256/package.json +21 -0
- package/examples/13-jwt-es256/src/index.ts +21 -0
- package/examples/13-jwt-es256/tests/pulse.harness.ts +54 -0
- package/examples/13-jwt-es256/tsconfig.json +14 -0
- package/examples/README.md +36 -0
- package/package.json +66 -6
- package/project-config.schema.json +981 -0
- package/release-manifest.json +441 -0
- package/src/command-spec.js +279 -0
- package/src/completion.js +113 -0
- package/src/diagnostics.js +350 -0
- package/src/documentation.js +45 -0
- package/src/index.d.ts +133 -0
- package/src/index.js +15 -0
- package/src/internal/command-executor.d.ts +29 -0
- package/src/internal/command-executor.js +143 -0
- package/src/internal/command-plan.d.ts +9 -0
- package/src/internal/command-plan.js +73 -0
- package/src/internal/command-reporter.d.ts +15 -0
- package/src/internal/command-reporter.js +133 -0
- package/src/internal/command-request.d.ts +35 -0
- package/src/internal/command-request.js +154 -0
- package/src/internal/node-http.js +54 -0
- package/src/internal/project-context.d.ts +66 -0
- package/src/internal/project-context.js +175 -0
- package/src/project-config-reference.js +4 -0
- package/src/project-config-schema.d.ts +23 -0
- package/src/project-config-schema.js +271 -0
- package/src/project-config.js +724 -0
- package/src/project-execution.js +2946 -0
- package/src/provider-drivers.js +3 -0
- package/src/target-support.js +3 -0
- package/src/typescript-module-loader.js +269 -0
- package/src/workflow.js +78 -0
- package/src/workspace.js +82 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Router lowering
|
|
2
|
+
|
|
3
|
+
Uses the static `Router` authoring surface to define terminal middleware, route
|
|
4
|
+
fallthrough, error handling, mounts, parameters, wildcards, and method-specific
|
|
5
|
+
routes. Pulse lowers the flat Router execution graph into the same canonical
|
|
6
|
+
native plan used by single-handler applications.
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pulse doctor
|
|
12
|
+
pulse inspect
|
|
13
|
+
pulse test
|
|
14
|
+
pulse dev
|
|
15
|
+
pulse compile
|
|
16
|
+
pulse build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The inspect result exposes Router execution entries and route/middleware
|
|
20
|
+
ownership for effects and continuations. `return next()` terminates the current
|
|
21
|
+
handler and advances dispatch; it never resumes.
|
|
22
|
+
|
|
23
|
+
<!-- pulse-doc-run {"project":"examples/09-router-lowering","args":["inspect","--json"]} -->
|
|
24
|
+
```bash
|
|
25
|
+
pulse inspect --json
|
|
26
|
+
```
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"status": "ok",
|
|
30
|
+
"provider": {
|
|
31
|
+
"id": "node"
|
|
32
|
+
},
|
|
33
|
+
"compiler": {
|
|
34
|
+
"authoring": {
|
|
35
|
+
"kind": "router"
|
|
36
|
+
},
|
|
37
|
+
"routing": {
|
|
38
|
+
"version": "pulse.router-authoring.v2",
|
|
39
|
+
"routes": [
|
|
40
|
+
{
|
|
41
|
+
"method": "GET",
|
|
42
|
+
"path": "/api/health"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"effectCount": 1,
|
|
47
|
+
"continuationCount": 1
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The documented test result below is also executed by the documentation gate.
|
|
53
|
+
|
|
54
|
+
<!-- pulse-doc-run {"project":"examples/09-router-lowering","args":["test","--json"]} -->
|
|
55
|
+
```bash
|
|
56
|
+
pulse test --json
|
|
57
|
+
```
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"status": "passed",
|
|
61
|
+
"provider": "node",
|
|
62
|
+
"summary": {
|
|
63
|
+
"total": 9,
|
|
64
|
+
"passed": 9,
|
|
65
|
+
"failed": 0
|
|
66
|
+
},
|
|
67
|
+
"cases": [
|
|
68
|
+
{
|
|
69
|
+
"name": "mounted-health",
|
|
70
|
+
"status": "passed",
|
|
71
|
+
"response": {
|
|
72
|
+
"status": 200
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Wasm size
|
|
80
|
+
|
|
81
|
+
| Artifact | Default build | `--experimental-native-size` | Reduction |
|
|
82
|
+
|---|---:|---:|---:|
|
|
83
|
+
| `canonical-native.wasm` | 6.1 KiB (6,271 bytes) | 5.4 KiB (5,496 bytes) | 12.4% |
|
|
84
|
+
|
|
85
|
+
The executable documentation gate rebuilds and measures both variants on
|
|
86
|
+
`1.0.0-beta.2`. These are uncompressed on-disk sizes, not transfer sizes or
|
|
87
|
+
platform limits.
|
|
88
|
+
|
|
89
|
+
## Router application
|
|
90
|
+
|
|
91
|
+
<!-- pulse-doc-source: examples/09-router-lowering/src/index.ts -->
|
|
92
|
+
```ts
|
|
93
|
+
import { Router } from '@pulse-compute/runtime'
|
|
94
|
+
|
|
95
|
+
interface User {
|
|
96
|
+
id: number
|
|
97
|
+
name: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const users = new Router()
|
|
101
|
+
const api = new Router()
|
|
102
|
+
const app = new Router()
|
|
103
|
+
|
|
104
|
+
app.use(async (ctx, next) => {
|
|
105
|
+
if (ctx.req.header('x-pulse-deny') === '1') {
|
|
106
|
+
return ctx.json(
|
|
107
|
+
{ error: 'denied by middleware' },
|
|
108
|
+
{ status: 401 },
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
return next()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
api.use(async (ctx, next) => {
|
|
115
|
+
if (ctx.req.header('x-api-state') === 'closed') {
|
|
116
|
+
return ctx.text('api closed', { status: 503 })
|
|
117
|
+
}
|
|
118
|
+
return next()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
users.get('/:id', async (ctx) => {
|
|
122
|
+
const id = ctx.param('id')
|
|
123
|
+
const user = await ctx
|
|
124
|
+
.fetch('https://users.example.test/users/' + id)
|
|
125
|
+
.json<User>()
|
|
126
|
+
return ctx.json({ route: 'user', id, user })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
api.get('/health', async (ctx, next) => {
|
|
130
|
+
if (ctx.req.header('x-health-handler') === 'fallback') {
|
|
131
|
+
return next()
|
|
132
|
+
}
|
|
133
|
+
return ctx.json({ ok: true, handler: 'primary' })
|
|
134
|
+
})
|
|
135
|
+
api.get('/health', async (ctx) => ctx.json({ ok: true, handler: 'fallback' }))
|
|
136
|
+
api.head('/health', async (ctx) => ctx.text('', {
|
|
137
|
+
status: 204,
|
|
138
|
+
headers: [['x-pulse-route', 'health']],
|
|
139
|
+
}))
|
|
140
|
+
api.post('/users', async (ctx) => ctx.json(
|
|
141
|
+
{ route: 'create', method: ctx.req.method },
|
|
142
|
+
{ status: 201 },
|
|
143
|
+
))
|
|
144
|
+
api.get('/files/*', async (ctx) => ctx.text(ctx.req.path))
|
|
145
|
+
api.get('/failure', async (ctx, next) => next('documented-failure'))
|
|
146
|
+
|
|
147
|
+
api.mount('/users', users)
|
|
148
|
+
app.mount('/api', api)
|
|
149
|
+
|
|
150
|
+
app.error(async (error, ctx, next) => {
|
|
151
|
+
if (error === 'documented-failure') {
|
|
152
|
+
return ctx.json({ error: 'handled' }, { status: 418 })
|
|
153
|
+
}
|
|
154
|
+
return next(error)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
export default app
|
|
158
|
+
```
|
|
159
|
+
<!-- /pulse-doc-source -->
|
|
160
|
+
|
|
161
|
+
## Project config
|
|
162
|
+
|
|
163
|
+
<!-- pulse-doc-source: examples/09-router-lowering/.pulse/config.ts -->
|
|
164
|
+
```ts
|
|
165
|
+
import { defineConfig } from '@pulse-compute/pulse'
|
|
166
|
+
|
|
167
|
+
export default defineConfig((_scope) => ({
|
|
168
|
+
pulse: {
|
|
169
|
+
entry: 'src/index.ts',
|
|
170
|
+
tests: 'tests/pulse.harness.ts',
|
|
171
|
+
defaultProfile: 'local',
|
|
172
|
+
strict: true,
|
|
173
|
+
},
|
|
174
|
+
local: {
|
|
175
|
+
host: 'node',
|
|
176
|
+
target: 'native',
|
|
177
|
+
outDir: 'dist',
|
|
178
|
+
dev: {
|
|
179
|
+
fetches: {
|
|
180
|
+
'https://users.example.test/users/7': {
|
|
181
|
+
value: { id: 7, name: 'Ada' },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}))
|
|
187
|
+
```
|
|
188
|
+
<!-- /pulse-doc-source -->
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
## Test harness
|
|
192
|
+
|
|
193
|
+
<!-- pulse-doc-source: examples/09-router-lowering/tests/pulse.harness.ts -->
|
|
194
|
+
```ts
|
|
195
|
+
export default {
|
|
196
|
+
cases: [
|
|
197
|
+
{
|
|
198
|
+
name: 'mounted-health',
|
|
199
|
+
request: { method: 'GET', path: '/api/health' },
|
|
200
|
+
expect: {
|
|
201
|
+
status: 200,
|
|
202
|
+
json: { ok: true, handler: 'primary' },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'middleware-short-circuit',
|
|
207
|
+
request: {
|
|
208
|
+
method: 'GET',
|
|
209
|
+
path: '/api/health',
|
|
210
|
+
headers: { 'x-pulse-deny': '1' },
|
|
211
|
+
},
|
|
212
|
+
expect: {
|
|
213
|
+
status: 401,
|
|
214
|
+
json: { error: 'denied by middleware' },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'scoped-middleware-short-circuit',
|
|
219
|
+
request: {
|
|
220
|
+
method: 'GET',
|
|
221
|
+
path: '/api/health',
|
|
222
|
+
headers: { 'x-api-state': 'closed' },
|
|
223
|
+
},
|
|
224
|
+
expect: { status: 503, text: 'api closed' },
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'route-fallthrough',
|
|
228
|
+
request: {
|
|
229
|
+
method: 'GET',
|
|
230
|
+
path: '/api/health',
|
|
231
|
+
headers: { 'x-health-handler': 'fallback' },
|
|
232
|
+
},
|
|
233
|
+
expect: {
|
|
234
|
+
status: 200,
|
|
235
|
+
json: { ok: true, handler: 'fallback' },
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'error-middleware',
|
|
240
|
+
request: { method: 'GET', path: '/api/failure' },
|
|
241
|
+
expect: { status: 418, json: { error: 'handled' } },
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'parameter-and-fetch',
|
|
245
|
+
request: { method: 'GET', path: '/api/users/7' },
|
|
246
|
+
fetches: {
|
|
247
|
+
'https://users.example.test/users/7': {
|
|
248
|
+
value: { id: 7, name: 'Ada' },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
expect: {
|
|
252
|
+
status: 200,
|
|
253
|
+
json: {
|
|
254
|
+
route: 'user',
|
|
255
|
+
id: '7',
|
|
256
|
+
user: { id: 7, name: 'Ada' },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'method-specific-post',
|
|
262
|
+
request: { method: 'POST', path: '/api/users' },
|
|
263
|
+
expect: {
|
|
264
|
+
status: 201,
|
|
265
|
+
json: { route: 'create', method: 'POST' },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: 'trailing-wildcard',
|
|
270
|
+
request: {
|
|
271
|
+
method: 'GET',
|
|
272
|
+
path: '/api/files/guides/start',
|
|
273
|
+
},
|
|
274
|
+
expect: {
|
|
275
|
+
status: 200,
|
|
276
|
+
text: '/api/files/guides/start',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'default-not-found',
|
|
281
|
+
request: { method: 'GET', path: '/missing' },
|
|
282
|
+
expect: { status: 404, text: 'Not Found' },
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
<!-- /pulse-doc-source -->
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pulse-examples/09-router-lowering",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"doctor": "pulse doctor",
|
|
8
|
+
"inspect": "pulse inspect",
|
|
9
|
+
"test": "pulse test",
|
|
10
|
+
"dev": "pulse dev",
|
|
11
|
+
"compile": "pulse compile",
|
|
12
|
+
"build": "pulse build"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@pulse-compute/runtime": "1.0.0-beta.2",
|
|
16
|
+
"@pulse-compute/pulse": "1.0.0-beta.2"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@pulse-compute/cli": "1.0.0-beta.2",
|
|
20
|
+
"typescript": "5.9.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Router } from '@pulse-compute/runtime'
|
|
2
|
+
|
|
3
|
+
interface User {
|
|
4
|
+
id: number
|
|
5
|
+
name: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const users = new Router()
|
|
9
|
+
const api = new Router()
|
|
10
|
+
const app = new Router()
|
|
11
|
+
|
|
12
|
+
app.use(async (ctx, next) => {
|
|
13
|
+
if (ctx.req.header('x-pulse-deny') === '1') {
|
|
14
|
+
return ctx.json(
|
|
15
|
+
{ error: 'denied by middleware' },
|
|
16
|
+
{ status: 401 },
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
return next()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
api.use(async (ctx, next) => {
|
|
23
|
+
if (ctx.req.header('x-api-state') === 'closed') {
|
|
24
|
+
return ctx.text('api closed', { status: 503 })
|
|
25
|
+
}
|
|
26
|
+
return next()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
users.get('/:id', async (ctx) => {
|
|
30
|
+
const id = ctx.param('id')
|
|
31
|
+
const user = await ctx
|
|
32
|
+
.fetch('https://users.example.test/users/' + id)
|
|
33
|
+
.json<User>()
|
|
34
|
+
return ctx.json({ route: 'user', id, user })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
api.get('/health', async (ctx, next) => {
|
|
38
|
+
if (ctx.req.header('x-health-handler') === 'fallback') {
|
|
39
|
+
return next()
|
|
40
|
+
}
|
|
41
|
+
return ctx.json({ ok: true, handler: 'primary' })
|
|
42
|
+
})
|
|
43
|
+
api.get('/health', async (ctx) => ctx.json({ ok: true, handler: 'fallback' }))
|
|
44
|
+
api.head('/health', async (ctx) => ctx.text('', {
|
|
45
|
+
status: 204,
|
|
46
|
+
headers: [['x-pulse-route', 'health']],
|
|
47
|
+
}))
|
|
48
|
+
api.post('/users', async (ctx) => ctx.json(
|
|
49
|
+
{ route: 'create', method: ctx.req.method },
|
|
50
|
+
{ status: 201 },
|
|
51
|
+
))
|
|
52
|
+
api.get('/files/*', async (ctx) => ctx.text(ctx.req.path))
|
|
53
|
+
api.get('/failure', async (ctx, next) => next('documented-failure'))
|
|
54
|
+
|
|
55
|
+
api.mount('/users', users)
|
|
56
|
+
app.mount('/api', api)
|
|
57
|
+
|
|
58
|
+
app.error(async (error, ctx, next) => {
|
|
59
|
+
if (error === 'documented-failure') {
|
|
60
|
+
return ctx.json({ error: 'handled' }, { status: 418 })
|
|
61
|
+
}
|
|
62
|
+
return next(error)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export default app
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
cases: [
|
|
3
|
+
{
|
|
4
|
+
name: 'mounted-health',
|
|
5
|
+
request: { method: 'GET', path: '/api/health' },
|
|
6
|
+
expect: {
|
|
7
|
+
status: 200,
|
|
8
|
+
json: { ok: true, handler: 'primary' },
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'middleware-short-circuit',
|
|
13
|
+
request: {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
path: '/api/health',
|
|
16
|
+
headers: { 'x-pulse-deny': '1' },
|
|
17
|
+
},
|
|
18
|
+
expect: {
|
|
19
|
+
status: 401,
|
|
20
|
+
json: { error: 'denied by middleware' },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'scoped-middleware-short-circuit',
|
|
25
|
+
request: {
|
|
26
|
+
method: 'GET',
|
|
27
|
+
path: '/api/health',
|
|
28
|
+
headers: { 'x-api-state': 'closed' },
|
|
29
|
+
},
|
|
30
|
+
expect: { status: 503, text: 'api closed' },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'route-fallthrough',
|
|
34
|
+
request: {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
path: '/api/health',
|
|
37
|
+
headers: { 'x-health-handler': 'fallback' },
|
|
38
|
+
},
|
|
39
|
+
expect: {
|
|
40
|
+
status: 200,
|
|
41
|
+
json: { ok: true, handler: 'fallback' },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'error-middleware',
|
|
46
|
+
request: { method: 'GET', path: '/api/failure' },
|
|
47
|
+
expect: { status: 418, json: { error: 'handled' } },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'parameter-and-fetch',
|
|
51
|
+
request: { method: 'GET', path: '/api/users/7' },
|
|
52
|
+
fetches: {
|
|
53
|
+
'https://users.example.test/users/7': {
|
|
54
|
+
value: { id: 7, name: 'Ada' },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
expect: {
|
|
58
|
+
status: 200,
|
|
59
|
+
json: {
|
|
60
|
+
route: 'user',
|
|
61
|
+
id: '7',
|
|
62
|
+
user: { id: 7, name: 'Ada' },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'method-specific-post',
|
|
68
|
+
request: { method: 'POST', path: '/api/users' },
|
|
69
|
+
expect: {
|
|
70
|
+
status: 201,
|
|
71
|
+
json: { route: 'create', method: 'POST' },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'trailing-wildcard',
|
|
76
|
+
request: {
|
|
77
|
+
method: 'GET',
|
|
78
|
+
path: '/api/files/guides/start',
|
|
79
|
+
},
|
|
80
|
+
expect: {
|
|
81
|
+
status: 200,
|
|
82
|
+
text: '/api/files/guides/start',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'default-not-found',
|
|
87
|
+
request: { method: 'GET', path: '/missing' },
|
|
88
|
+
expect: { status: 404, text: 'Not Found' },
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineConfig } from '@pulse-compute/pulse'
|
|
2
|
+
|
|
3
|
+
export default defineConfig((_scope) => ({
|
|
4
|
+
pulse: {
|
|
5
|
+
entry: 'src/index.ts',
|
|
6
|
+
schema: 'src/schemas.ts',
|
|
7
|
+
tests: 'tests/pulse.harness.ts',
|
|
8
|
+
defaultProfile: 'node-javascript',
|
|
9
|
+
strict: true,
|
|
10
|
+
},
|
|
11
|
+
'node-javascript': {
|
|
12
|
+
host: 'node',
|
|
13
|
+
target: 'javascript',
|
|
14
|
+
outDir: 'dist-node-javascript',
|
|
15
|
+
dev: { networkFetch: false },
|
|
16
|
+
},
|
|
17
|
+
'node-native': {
|
|
18
|
+
host: 'node',
|
|
19
|
+
target: 'native',
|
|
20
|
+
outDir: 'dist-node-native',
|
|
21
|
+
dev: { networkFetch: false },
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Entities tools facade
|
|
2
|
+
|
|
3
|
+
This experimental example declares two schema-bound operations with
|
|
4
|
+
`@pulse-compute/entities`, exposes them through the first-party JSON-RPC
|
|
5
|
+
adapter, and projects their emitted static catalog into a dependency-free tools
|
|
6
|
+
facade. The facade invokes the governed request boundary; it does not call
|
|
7
|
+
application handlers directly.
|
|
8
|
+
|
|
9
|
+
The application contributes:
|
|
10
|
+
|
|
11
|
+
- `system.status`, which accepts no input and completes with `null`;
|
|
12
|
+
- `customer.lookup`, which validates named input and output and performs one
|
|
13
|
+
governed `ctx.fetch` effect.
|
|
14
|
+
|
|
15
|
+
## Candidate workflow
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pulse inspect
|
|
19
|
+
node ../../wasm/scripts/run-wasm-tests.cjs --task entities-orchestration-demo --no-report
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`pulse inspect` reports the static entity catalog, declared schemas, handler
|
|
23
|
+
effects, and target evidence. The focused orchestration gate builds and consumes
|
|
24
|
+
the catalog, invokes both operations through the Node JavaScript request
|
|
25
|
+
boundary, validates the governed fetch, and compiles/inspects the package-owned
|
|
26
|
+
Native artifact.
|
|
27
|
+
|
|
28
|
+
The ordinary JavaScript `test`/`dev` application loader currently requests the
|
|
29
|
+
unexported physical package entry, and ordinary Native builds do not yet adopt
|
|
30
|
+
the Entities intrinsic. I11 records those product-integration blockers instead
|
|
31
|
+
of widening package exports or compiler/provider authority. A JavaScript build
|
|
32
|
+
can still emit the source package, catalog, and inspection artifacts; no command
|
|
33
|
+
may fall back to another target.
|
|
34
|
+
|
|
35
|
+
## Wasm size
|
|
36
|
+
|
|
37
|
+
The default `node-javascript` profile emits **no application Wasm artifact**.
|
|
38
|
+
The ordinary `node-native` build currently fails closed at the unadopted
|
|
39
|
+
Entities intrinsic. The focused gate's package-owned Native artifact exercises
|
|
40
|
+
a different boundary and is not comparable to an emitted application guest.
|
|
41
|
+
Consequently, `--experimental-native-size` has no application artifact to
|
|
42
|
+
optimize for this example.
|
|
43
|
+
|
|
44
|
+
## Entity application
|
|
45
|
+
|
|
46
|
+
<!-- pulse-doc-source: examples/10-entities-tools/src/index.ts -->
|
|
47
|
+
```ts
|
|
48
|
+
import { EntityRouter, jsonRpc } from '@pulse-compute/entities'
|
|
49
|
+
import { lookupCustomer, systemStatus } from './handlers.js'
|
|
50
|
+
|
|
51
|
+
const rpc = new EntityRouter({
|
|
52
|
+
adapter: jsonRpc({ namedParamsOnly: true, acceptEmptyObjectForNoInput: true }),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
rpc.on('system.status', {
|
|
56
|
+
input: null,
|
|
57
|
+
output: null,
|
|
58
|
+
metadata: {
|
|
59
|
+
title: 'Check system status',
|
|
60
|
+
description: 'Checks that the governed entity request boundary is available.',
|
|
61
|
+
mcp: { readOnlyHint: true },
|
|
62
|
+
},
|
|
63
|
+
}, systemStatus)
|
|
64
|
+
|
|
65
|
+
rpc.on('customer.lookup', {
|
|
66
|
+
input: 'tools.CustomerLookupInput',
|
|
67
|
+
output: 'tools.CustomerLookupOutput',
|
|
68
|
+
metadata: {
|
|
69
|
+
title: 'Look up customer',
|
|
70
|
+
description: 'Looks up one customer through the configured directory backend.',
|
|
71
|
+
mcp: { readOnlyHint: true },
|
|
72
|
+
},
|
|
73
|
+
}, lookupCustomer)
|
|
74
|
+
|
|
75
|
+
export default async function handler(ctx: unknown) {
|
|
76
|
+
return rpc.handle(ctx as never)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
<!-- /pulse-doc-source -->
|
|
80
|
+
|
|
81
|
+
## Project config
|
|
82
|
+
|
|
83
|
+
<!-- pulse-doc-source: examples/10-entities-tools/.pulse/config.ts -->
|
|
84
|
+
```ts
|
|
85
|
+
import { defineConfig } from '@pulse-compute/pulse'
|
|
86
|
+
|
|
87
|
+
export default defineConfig((_scope) => ({
|
|
88
|
+
pulse: {
|
|
89
|
+
entry: 'src/index.ts',
|
|
90
|
+
schema: 'src/schemas.ts',
|
|
91
|
+
tests: 'tests/pulse.harness.ts',
|
|
92
|
+
defaultProfile: 'node-javascript',
|
|
93
|
+
strict: true,
|
|
94
|
+
},
|
|
95
|
+
'node-javascript': {
|
|
96
|
+
host: 'node',
|
|
97
|
+
target: 'javascript',
|
|
98
|
+
outDir: 'dist-node-javascript',
|
|
99
|
+
dev: { networkFetch: false },
|
|
100
|
+
},
|
|
101
|
+
'node-native': {
|
|
102
|
+
host: 'node',
|
|
103
|
+
target: 'native',
|
|
104
|
+
outDir: 'dist-node-native',
|
|
105
|
+
dev: { networkFetch: false },
|
|
106
|
+
},
|
|
107
|
+
}))
|
|
108
|
+
```
|
|
109
|
+
<!-- /pulse-doc-source -->
|
|
110
|
+
|
|
111
|
+
## Test harness
|
|
112
|
+
|
|
113
|
+
<!-- pulse-doc-source: examples/10-entities-tools/tests/pulse.harness.ts -->
|
|
114
|
+
```ts
|
|
115
|
+
export default { cases: [
|
|
116
|
+
{
|
|
117
|
+
name: 'system-status',
|
|
118
|
+
request: {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
path: '/',
|
|
121
|
+
headers: { 'content-type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'system.status', id: 'status-doc' }),
|
|
123
|
+
},
|
|
124
|
+
expect: {
|
|
125
|
+
status: 200,
|
|
126
|
+
json: { jsonrpc: '2.0', result: null, id: 'status-doc' },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'customer-lookup',
|
|
131
|
+
request: {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
path: '/',
|
|
134
|
+
headers: { 'content-type': 'application/json' },
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
jsonrpc: '2.0',
|
|
137
|
+
method: 'customer.lookup',
|
|
138
|
+
params: { email: 'ada@example.test' },
|
|
139
|
+
id: 'lookup-doc',
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
fetches: {
|
|
143
|
+
'https://directory.example.test/customers/ada@example.test': { value: 'Ada Lovelace' },
|
|
144
|
+
},
|
|
145
|
+
expect: {
|
|
146
|
+
status: 200,
|
|
147
|
+
json: {
|
|
148
|
+
jsonrpc: '2.0',
|
|
149
|
+
result: { email: 'ada@example.test', displayName: 'Ada Lovelace' },
|
|
150
|
+
id: 'lookup-doc',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
] }
|
|
155
|
+
```
|
|
156
|
+
<!-- /pulse-doc-source -->
|
|
157
|
+
|
|
158
|
+
## Facade boundary
|
|
159
|
+
|
|
160
|
+
`tools-facade.cjs` is an orchestration example, not a complete MCP server. It
|
|
161
|
+
does not add listeners, SSE, sessions, tasks, resources, prompts, sampling,
|
|
162
|
+
authorization, transport negotiation, or protocol lifecycle state to Pulse
|
|
163
|
+
runtime core. A future MCP or tools facade remains an adapter outside the
|
|
164
|
+
entity engine, just as a future Worker/event adapter would bind a different
|
|
165
|
+
request or event protocol without changing entity handler semantics.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pulse-examples/10-entities-tools",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"doctor": "pulse doctor",
|
|
8
|
+
"inspect": "pulse inspect",
|
|
9
|
+
"test": "pulse test",
|
|
10
|
+
"dev": "pulse dev",
|
|
11
|
+
"build": "pulse build"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@pulse-compute/entities": "1.0.0-beta.2",
|
|
15
|
+
"@pulse-compute/pulse": "1.0.0-beta.2"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@pulse-compute/cli": "1.0.0-beta.2",
|
|
19
|
+
"typescript": "5.9.3"
|
|
20
|
+
}
|
|
21
|
+
}
|