@pulse-compute/cli 0.0.0 → 1.0.0-beta.1
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 +539 -0
- package/CHANGELOG.md +47 -0
- package/README.md +70 -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 +381 -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 +243 -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 +62 -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 +184 -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 +84 -0
- package/docs/maintainers/maintenance-policy.json +778 -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 +250 -0
- package/docs/maintainers/release-manifest.md +88 -0
- package/docs/maintainers/repository-setup.md +145 -0
- package/docs/maintainers/scope-policy.md +93 -0
- package/docs/maintainers/support-and-triage.md +74 -0
- package/docs/maintainers/testing.md +288 -0
- package/docs/packages/README.md +59 -0
- package/docs/packages/assets.md +85 -0
- package/docs/packages/cli.md +190 -0
- package/docs/packages/crypto.md +71 -0
- package/docs/packages/entities.md +129 -0
- package/docs/packages/grip.md +82 -0
- package/docs/packages/implementation-packages.md +113 -0
- package/docs/packages/jwt.md +55 -0
- package/docs/packages/provider-fastly.md +217 -0
- package/docs/packages/pulse.md +159 -0
- package/docs/packages/runtime.md +256 -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 +14 -0
- package/docs/reference/environment.md +347 -0
- package/docs/reference/handler-authoring.md +264 -0
- package/docs/reference/project-config.md +554 -0
- package/docs/reference/project-config.schema.json +918 -0
- package/docs/reference/release-manifest.json +427 -0
- package/docs/reference/shell-completion.md +23 -0
- package/documentation-site.json +661 -0
- package/documentation-versions.json +14 -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 +918 -0
- package/release-manifest.json +427 -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 +2930 -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,515 @@
|
|
|
1
|
+
# Pulse Vision
|
|
2
|
+
|
|
3
|
+
> **Status:** North-star architecture and product direction.
|
|
4
|
+
> This document describes the intended shape of Pulse from the Beta
|
|
5
|
+
> toward a compatibility-bearing 1.0. It is directional rather than a
|
|
6
|
+
> compatibility promise for every current release.
|
|
7
|
+
|
|
8
|
+
Pulse is a portable application contract for progressively constrained compute.
|
|
9
|
+
|
|
10
|
+
It lets developers begin with familiar TypeScript, choose an execution target deliberately, and move toward compact native WebAssembly without hiding where the source stops being eligible. Pulse does not attempt to make every host identical, compile the entire JavaScript ecosystem, or silently change deployment architecture. It makes boundaries explicit so teams can ship now and reduce execution debt over time.
|
|
11
|
+
|
|
12
|
+
The long-term goal is not merely “TypeScript to Wasm.” It is a disciplined path from familiar application code to small, host-realizable programs across servers, edge platforms, and constrained devices.
|
|
13
|
+
|
|
14
|
+
## The problem
|
|
15
|
+
|
|
16
|
+
Application code tends to accumulate host assumptions gradually:
|
|
17
|
+
|
|
18
|
+
- framework-specific request and response objects;
|
|
19
|
+
- implicit event-loop and Promise behavior;
|
|
20
|
+
- direct access to environment variables and secrets;
|
|
21
|
+
- provider SDKs embedded in business logic;
|
|
22
|
+
- large dependencies added for a single feature;
|
|
23
|
+
- opaque JSON contracts;
|
|
24
|
+
- deployment behavior that changes without a clear source-level milestone.
|
|
25
|
+
|
|
26
|
+
Those assumptions are easy to introduce and difficult to remove later. By the time a team wants a smaller artifact, a more constrained host, or a different provider, the application and execution environment may be inseparable.
|
|
27
|
+
|
|
28
|
+
Traditional approaches often force an all-or-nothing choice:
|
|
29
|
+
|
|
30
|
+
1. stay in a general JavaScript runtime and accept its operational cost; or
|
|
31
|
+
2. rewrite the application into a constrained language or platform-specific SDK before receiving any native benefit.
|
|
32
|
+
|
|
33
|
+
Pulse aims for a third path.
|
|
34
|
+
|
|
35
|
+
## The thesis
|
|
36
|
+
|
|
37
|
+
Pulse provides one stable application model and several explicit execution targets.
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
familiar TypeScript application
|
|
41
|
+
↓
|
|
42
|
+
Pulse runtime semantics
|
|
43
|
+
↓
|
|
44
|
+
eligibility analysis
|
|
45
|
+
↓
|
|
46
|
+
explicitly selected target
|
|
47
|
+
↓
|
|
48
|
+
native Wasm, JavaScript runtime, or another host realization
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The source remains recognizable. The target determines which parts can be lowered and how the host realizes capabilities.
|
|
52
|
+
|
|
53
|
+
Pulse should tell the operator:
|
|
54
|
+
|
|
55
|
+
- what target was selected;
|
|
56
|
+
- what was lowered;
|
|
57
|
+
- what could not be lowered;
|
|
58
|
+
- the first exact boundary where native eligibility ended;
|
|
59
|
+
- which alternate targets are available;
|
|
60
|
+
- what size, validation, or execution tradeoffs the selection introduces.
|
|
61
|
+
|
|
62
|
+
Pulse must not silently fall back from native Wasm to JavaScript. Changing the target is an operator decision expressed in configuration or an explicit CLI invocation.
|
|
63
|
+
|
|
64
|
+
## Familiar TypeScript is the migration surface
|
|
65
|
+
|
|
66
|
+
Pulse should feel like ordinary TypeScript application code, not a compiler DSL.
|
|
67
|
+
|
|
68
|
+
The intended handler shape is async:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
app.get('/users/:id', async (ctx) => {
|
|
72
|
+
const user = await ctx
|
|
73
|
+
.fetch(`/origin/users/${ctx.param('id')}`)
|
|
74
|
+
.json<User>('app.User')
|
|
75
|
+
|
|
76
|
+
return ctx.json({ user })
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The same source has different mechanics under different targets.
|
|
81
|
+
|
|
82
|
+
### Native target
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
async wrapper
|
|
86
|
+
→ erased by lowering
|
|
87
|
+
|
|
88
|
+
await trusted Pulse effect
|
|
89
|
+
→ effect emission
|
|
90
|
+
→ state-machine suspension
|
|
91
|
+
→ host completion
|
|
92
|
+
→ continuation resume
|
|
93
|
+
|
|
94
|
+
Promise runtime
|
|
95
|
+
→ absent
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### JavaScript target
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
async wrapper
|
|
102
|
+
→ ordinary async JavaScript
|
|
103
|
+
|
|
104
|
+
await trusted Pulse effect
|
|
105
|
+
→ Promise-backed runtime operation
|
|
106
|
+
|
|
107
|
+
continuation
|
|
108
|
+
→ JavaScript runtime
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The mechanics differ, but Pulse-visible semantics must not:
|
|
112
|
+
|
|
113
|
+
- route and middleware order;
|
|
114
|
+
- capability ordering;
|
|
115
|
+
- error-lane behavior;
|
|
116
|
+
- status and headers;
|
|
117
|
+
- body ownership;
|
|
118
|
+
- schema results;
|
|
119
|
+
- redaction;
|
|
120
|
+
- completion and fallthrough.
|
|
121
|
+
|
|
122
|
+
All Pulse handlers should be async-shaped even when a particular handler contains no `await`. That keeps the source stable when capability work is added later.
|
|
123
|
+
|
|
124
|
+
## A small control-flow grammar
|
|
125
|
+
|
|
126
|
+
Pulse intentionally keeps routing control separate from capability suspension.
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
await effect → suspend for external capability work
|
|
130
|
+
return next() → permanently transfer Router control
|
|
131
|
+
return response → complete the request
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Terminal middleware avoids onion-style resumption:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
app.use(async (ctx, next) => {
|
|
138
|
+
const session = await ctx.kv.get('sessions', ctx.req.header('x-session'))
|
|
139
|
+
|
|
140
|
+
if (!session) {
|
|
141
|
+
return ctx.json({ error: 'unauthorized' }, { status: 401 })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return next()
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`next()` is not awaited and does not return control to the middleware. This removes suspended middleware frames, post-response mutation, ambiguous cleanup order, and accidental double responses.
|
|
149
|
+
|
|
150
|
+
The constraint simplifies native lowering, JavaScript execution, inspection, and cross-target conformance.
|
|
151
|
+
|
|
152
|
+
## Explicit execution targets
|
|
153
|
+
|
|
154
|
+
Pulse should classify execution honestly.
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
native
|
|
158
|
+
fully lowered into Pulse-owned Wasm
|
|
159
|
+
|
|
160
|
+
javascript
|
|
161
|
+
executed by a JavaScript runtime using the same Pulse semantics
|
|
162
|
+
|
|
163
|
+
sidecar
|
|
164
|
+
executed across an explicit serialized boundary
|
|
165
|
+
|
|
166
|
+
rejected
|
|
167
|
+
unsupported for the selected target
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Not every target must exist for every provider.
|
|
171
|
+
|
|
172
|
+
A native build that encounters unsupported code should fail precisely:
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
PULSE_NATIVE_AWAIT_UNSUPPORTED
|
|
176
|
+
|
|
177
|
+
Route:
|
|
178
|
+
POST /reports
|
|
179
|
+
|
|
180
|
+
Handler:
|
|
181
|
+
createReport
|
|
182
|
+
|
|
183
|
+
Unsupported boundary:
|
|
184
|
+
await reportingLibrary.render(data)
|
|
185
|
+
|
|
186
|
+
The selected native target supports await only on trusted Pulse effects.
|
|
187
|
+
|
|
188
|
+
Available paths:
|
|
189
|
+
- remove or isolate the dependency;
|
|
190
|
+
- replace it with a Pulse capability;
|
|
191
|
+
- provide package-owned lowering;
|
|
192
|
+
- explicitly select the JavaScript target;
|
|
193
|
+
- move the operation behind a sidecar boundary.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The failure is useful product output. It records the moment architectural debt entered the stack.
|
|
197
|
+
|
|
198
|
+
An operator can then deliberately choose a different target and continue shipping:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
export default defineConfig({
|
|
202
|
+
execution: {
|
|
203
|
+
target: 'javascript',
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Pulse still reports native eligibility while building the JavaScript target, so the path back toward native remains visible.
|
|
209
|
+
|
|
210
|
+
## Hosts are not equal
|
|
211
|
+
|
|
212
|
+
Pulse can target multiple hosts, but it must not pretend that every host has the same capabilities, execution models, or escape routes.
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
Node
|
|
216
|
+
native target: available
|
|
217
|
+
JavaScript target: available
|
|
218
|
+
host strengths: broad local and server integration
|
|
219
|
+
|
|
220
|
+
Fastly
|
|
221
|
+
native target: available
|
|
222
|
+
JavaScript target: available as an explicit larger-runtime choice
|
|
223
|
+
host strengths: edge HTTP, named backends, config, secrets, KV
|
|
224
|
+
|
|
225
|
+
ESP32
|
|
226
|
+
native target: architectural goal / reference host
|
|
227
|
+
JavaScript target: unavailable
|
|
228
|
+
host strengths: GPIO, timers, sensors, constrained event processing
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
This is not “write once, run anywhere.”
|
|
232
|
+
|
|
233
|
+
It is:
|
|
234
|
+
|
|
235
|
+
> Use one application contract, then let each host state clearly what it can realize.
|
|
236
|
+
|
|
237
|
+
A provider owns the concrete ABI and lifecycle. Pulse owns application semantics.
|
|
238
|
+
|
|
239
|
+
### Pulse owns
|
|
240
|
+
|
|
241
|
+
- Router topology and matching;
|
|
242
|
+
- terminal middleware;
|
|
243
|
+
- normal and error lanes;
|
|
244
|
+
- effects and continuations;
|
|
245
|
+
- request and response semantics;
|
|
246
|
+
- schema and JSON contracts;
|
|
247
|
+
- capability identities;
|
|
248
|
+
- eligibility diagnostics;
|
|
249
|
+
- portable inspection artifacts.
|
|
250
|
+
|
|
251
|
+
### The host owns
|
|
252
|
+
|
|
253
|
+
- sockets and HTTP stacks;
|
|
254
|
+
- interrupts and event queues;
|
|
255
|
+
- GPIO and sensors;
|
|
256
|
+
- clocks and timers;
|
|
257
|
+
- storage engines;
|
|
258
|
+
- concrete network APIs;
|
|
259
|
+
- process lifecycle;
|
|
260
|
+
- resource handles;
|
|
261
|
+
- ABI realization.
|
|
262
|
+
|
|
263
|
+
Host sovereignty is a feature. It prevents the runtime contract from becoming a lowest-common-denominator abstraction.
|
|
264
|
+
|
|
265
|
+
## Containment reduces friction later
|
|
266
|
+
|
|
267
|
+
Pulse is intentionally disciplined at boundaries:
|
|
268
|
+
|
|
269
|
+
- capability access goes through `ctx`;
|
|
270
|
+
- Router control uses terminal transfers;
|
|
271
|
+
- native suspension occurs only at trusted effects;
|
|
272
|
+
- schemas provide runtime truth;
|
|
273
|
+
- provider mechanics stay outside application logic;
|
|
274
|
+
- target changes are explicit;
|
|
275
|
+
- unsupported lowering stops at the first exact boundary.
|
|
276
|
+
|
|
277
|
+
This can feel stricter at the point of authoring, but it reduces friction later.
|
|
278
|
+
|
|
279
|
+
Containment gives the project:
|
|
280
|
+
|
|
281
|
+
- smaller native artifacts;
|
|
282
|
+
- deterministic lowering;
|
|
283
|
+
- simpler host adapters;
|
|
284
|
+
- better cross-target testing;
|
|
285
|
+
- clearer security and secret boundaries;
|
|
286
|
+
- visible dependency debt;
|
|
287
|
+
- less provider lock-in inside business logic;
|
|
288
|
+
- a practical path from general code toward constrained execution.
|
|
289
|
+
|
|
290
|
+
The goal is not maximal permissiveness. The goal is to make the cost and ownership of every escape understandable.
|
|
291
|
+
|
|
292
|
+
## Schemas are optimization and validation artifacts
|
|
293
|
+
|
|
294
|
+
TypeScript generics help the author but disappear at runtime. Pulse schemas provide runtime identity across targets.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const input = await ctx.req.json<CreateUser>('app.CreateUser')
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Native execution
|
|
301
|
+
|
|
302
|
+
The schema can drive:
|
|
303
|
+
|
|
304
|
+
- specialized decoding;
|
|
305
|
+
- projection;
|
|
306
|
+
- bounded layouts;
|
|
307
|
+
- smaller artifacts;
|
|
308
|
+
- predictable memory use;
|
|
309
|
+
- precise validation.
|
|
310
|
+
|
|
311
|
+
### JavaScript execution
|
|
312
|
+
|
|
313
|
+
The same schema can drive generated validation after `JSON.parse`.
|
|
314
|
+
|
|
315
|
+
Validation policy should be explicit:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
export default defineConfig({
|
|
319
|
+
schema: {
|
|
320
|
+
validation: 'strict', // strict | warn | off
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
- `strict`: reject invalid values;
|
|
326
|
+
- `warn`: continue but record a contract violation;
|
|
327
|
+
- `off`: parse only, while retaining schema visibility for tooling and eligibility analysis.
|
|
328
|
+
|
|
329
|
+
Schemas should specialize and verify JSON; they should not become the price of admission for JSON.
|
|
330
|
+
|
|
331
|
+
Pulse should support a gradient:
|
|
332
|
+
|
|
333
|
+
```text
|
|
334
|
+
strict projected schema
|
|
335
|
+
→ status-indexed schema variants
|
|
336
|
+
→ generic native JSON
|
|
337
|
+
→ explicit JavaScript parse-only behavior
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Response contracts should be status-aware rather than forcing unrelated success and failure payloads into one artificial shape:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
responses: {
|
|
344
|
+
200: 'app.User',
|
|
345
|
+
404: 'app.NotFound',
|
|
346
|
+
'5xx': 'app.ServerError',
|
|
347
|
+
default: 'json',
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Tooling should help observe, diff, propose, and verify these contracts over time.
|
|
352
|
+
|
|
353
|
+
## Eligibility is a first-class artifact
|
|
354
|
+
|
|
355
|
+
A Pulse build should produce more than a deployable binary.
|
|
356
|
+
|
|
357
|
+
It should also produce an architectural ledger:
|
|
358
|
+
|
|
359
|
+
```json
|
|
360
|
+
{
|
|
361
|
+
"selectedTarget": "javascript",
|
|
362
|
+
"nativeEligibility": {
|
|
363
|
+
"eligible": false,
|
|
364
|
+
"eligibleRoutes": 12,
|
|
365
|
+
"blockedRoutes": 1,
|
|
366
|
+
"firstUnsupportedBoundary": {
|
|
367
|
+
"route": "POST /reports",
|
|
368
|
+
"handler": "createReport",
|
|
369
|
+
"kind": "unsupported-import",
|
|
370
|
+
"specifier": "large-reporting-library"
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Teams should be able to see when a release moved from fully native-eligible to partially blocked, why it happened, and what must change to recover eligibility.
|
|
377
|
+
|
|
378
|
+
This turns constraint into feedback rather than punishment.
|
|
379
|
+
|
|
380
|
+
## Cross-target semantics must be enforced
|
|
381
|
+
|
|
382
|
+
Native and JavaScript targets must share one conformance corpus.
|
|
383
|
+
|
|
384
|
+
The same application cases should run through every supported realization:
|
|
385
|
+
|
|
386
|
+
```text
|
|
387
|
+
native Node
|
|
388
|
+
native Fastly
|
|
389
|
+
JavaScript Node
|
|
390
|
+
JavaScript Fastly
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The contract suite should compare Pulse-visible behavior:
|
|
394
|
+
|
|
395
|
+
- response status;
|
|
396
|
+
- ordered and repeated headers;
|
|
397
|
+
- structured and opaque bodies;
|
|
398
|
+
- route and middleware ordering;
|
|
399
|
+
- terminal `next()` behavior;
|
|
400
|
+
- error-lane transitions;
|
|
401
|
+
- capability ordering;
|
|
402
|
+
- schema success and failure;
|
|
403
|
+
- transport errors versus HTTP errors;
|
|
404
|
+
- redaction;
|
|
405
|
+
- compiler-owned 404 and 500 behavior.
|
|
406
|
+
|
|
407
|
+
Provider mechanics may differ. Pulse semantics may not.
|
|
408
|
+
|
|
409
|
+
Drift should fail CI and block release.
|
|
410
|
+
|
|
411
|
+
## ESP32 as a boundary witness
|
|
412
|
+
|
|
413
|
+
The ESP32 reference host exists to test whether Pulse is genuinely a constrained-compute model or merely an HTTP compiler.
|
|
414
|
+
|
|
415
|
+
Reference repository:
|
|
416
|
+
|
|
417
|
+
[pulsecompute/pulse-esp32-host](https://github.com/pulsecompute/pulse-esp32-host)
|
|
418
|
+
|
|
419
|
+
A device-oriented application might look like:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
app.on('gpio:button', async (ctx) => {
|
|
423
|
+
const enabled = await ctx.kv.get<boolean>('device', 'enabled')
|
|
424
|
+
|
|
425
|
+
await ctx.gpio.write('status-led', !enabled)
|
|
426
|
+
await ctx.kv.put('device', 'enabled', !enabled)
|
|
427
|
+
})
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
On an ESP32 host, those `await` expressions would not require a Promise runtime. They would lower into host operations and continuation states.
|
|
431
|
+
|
|
432
|
+
The event path must remain host-controlled:
|
|
433
|
+
|
|
434
|
+
```text
|
|
435
|
+
native interrupt service routine
|
|
436
|
+
→ host event queue
|
|
437
|
+
→ Pulse event frame
|
|
438
|
+
→ Wasm handler
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Pulse should never make reentrant calls from an interrupt directly into Wasm.
|
|
442
|
+
|
|
443
|
+
The ESP32 host is not a promise that every Pulse application runs on a microcontroller. It demonstrates that the abstractions still make sense when these assumptions are removed:
|
|
444
|
+
|
|
445
|
+
- no JavaScript fallback;
|
|
446
|
+
- no abundant memory;
|
|
447
|
+
- no cloud runtime;
|
|
448
|
+
- no HTTP-centric lifecycle;
|
|
449
|
+
- no generic operating-system services.
|
|
450
|
+
|
|
451
|
+
Fastly proves native edge execution. ESP32 tests the larger architectural claim.
|
|
452
|
+
|
|
453
|
+
## Product milestones
|
|
454
|
+
|
|
455
|
+
### Beta: explicit execution fluidity
|
|
456
|
+
|
|
457
|
+
The Beta proves:
|
|
458
|
+
|
|
459
|
+
- the public runtime and conventional Pulse application contracts;
|
|
460
|
+
- Router and terminal middleware;
|
|
461
|
+
- async-shaped handlers and trusted `await` lowering;
|
|
462
|
+
- effects, continuations, schemas, and package-owned capabilities;
|
|
463
|
+
- compact portable and provider-native Wasm;
|
|
464
|
+
- explicit Node and Fastly JavaScript execution and packaging;
|
|
465
|
+
- deliberate target selection with native-eligibility diagnostics;
|
|
466
|
+
- a shared four-mode conformance corpus;
|
|
467
|
+
- explicit failure at unsupported Native boundaries;
|
|
468
|
+
- no silent fallback.
|
|
469
|
+
|
|
470
|
+
The preview remains a deliberate pre-compatibility release. Passing its
|
|
471
|
+
technical gates does not authorize publication or deployment.
|
|
472
|
+
|
|
473
|
+
### 1.0: compatibility-bearing runtime contract
|
|
474
|
+
|
|
475
|
+
The first stable release begins when the Beta contract has been
|
|
476
|
+
exercised publicly and the runtime, CLI, target-selection, and conformance
|
|
477
|
+
policies are ready to carry compatibility obligations.
|
|
478
|
+
|
|
479
|
+
Sidecar partitioning, richer observability, additional hosts, and higher-order `@pulse-compute/pulse` ergonomics can continue after 1.0.
|
|
480
|
+
|
|
481
|
+
## What Pulse refuses to promise
|
|
482
|
+
|
|
483
|
+
Pulse does not promise:
|
|
484
|
+
|
|
485
|
+
- that arbitrary JavaScript becomes native Wasm;
|
|
486
|
+
- that every npm package is supported by every target;
|
|
487
|
+
- that every provider has the same capabilities;
|
|
488
|
+
- that JavaScript and native targets have identical size or startup cost;
|
|
489
|
+
- that TypeScript generics provide runtime validation;
|
|
490
|
+
- that unsupported code silently changes the deployment target;
|
|
491
|
+
- that a host abstraction erases host ownership;
|
|
492
|
+
- that all Pulse applications run on every device.
|
|
493
|
+
|
|
494
|
+
These refusals protect the useful promises.
|
|
495
|
+
|
|
496
|
+
## North star
|
|
497
|
+
|
|
498
|
+
Pulse should let a team say:
|
|
499
|
+
|
|
500
|
+
> We began with a familiar TypeScript application.
|
|
501
|
+
> We shipped it using an explicit execution target.
|
|
502
|
+
> Pulse showed us exactly where native eligibility ended.
|
|
503
|
+
> We removed dependencies, introduced trusted effects and schemas, and progressively constrained the program.
|
|
504
|
+
> The application model stayed stable while the artifact became smaller, more portable, and easier to host.
|
|
505
|
+
|
|
506
|
+
That is the vision:
|
|
507
|
+
|
|
508
|
+
```text
|
|
509
|
+
familiar TypeScript
|
|
510
|
+
+ explicit host capabilities
|
|
511
|
+
+ visible eligibility boundaries
|
|
512
|
+
+ deliberate execution targets
|
|
513
|
+
+ progressive containment
|
|
514
|
+
= constrained compute without an all-or-nothing rewrite
|
|
515
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Structured and opaque bodies
|
|
2
|
+
|
|
3
|
+
Pulse distinguishes bodies that application code may inspect from bodies that must remain host-owned. That distinction is part of the provider-neutral contract and is visible in types, compiler metadata, tests, and diagnostics.
|
|
4
|
+
|
|
5
|
+
A useful rule is:
|
|
6
|
+
|
|
7
|
+
> **Inspect it as a bounded structured value, or pass it through as an opaque response. Do not silently switch between the two.**
|
|
8
|
+
|
|
9
|
+
## Ownership transitions
|
|
10
|
+
|
|
11
|
+
All network body bytes begin under host or provider ownership. A supported
|
|
12
|
+
Pulse operation either converts them once into bounded request-owned data or
|
|
13
|
+
preserves an opaque host-owned handle. Ownership never moves through a provider
|
|
14
|
+
SDK object in userland.
|
|
15
|
+
|
|
16
|
+
| Boundary | Owner before | Application operation | Owner after |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| Incoming request text/JSON | Request host owns body bytes | `ctx.req.text()` or `ctx.req.json()` | Current request owns the bounded structured value and read cache. |
|
|
19
|
+
| Fetched response text/JSON | Provider adapter owns response bytes | `.text()` or `.json()` on the fetch operation | Current request owns the bounded structured projection. |
|
|
20
|
+
| Outbound fetch JSON | Application owns a supported structured value | `ctx.fetch(url, { json, schema })` | Pulse encodes the semantic value; the provider owns dispatched body bytes. |
|
|
21
|
+
| Application text/JSON response | Application owns a supported structured value | `ctx.text()`, `ctx.json()`, or `ctx.response()` | Pulse returns a terminal result; the provider owns response realization. |
|
|
22
|
+
| Opaque fetch or package response | Provider owns the body handle | Return the response directly | Provider retains ownership through terminal pass-through. |
|
|
23
|
+
|
|
24
|
+
Request-owned values and caches end with that request. They cannot be retained
|
|
25
|
+
for background work. Provider-owned opaque handles stay opaque: they cannot be converted into a structured body. Structured values cannot be promoted into
|
|
26
|
+
a userland stream.
|
|
27
|
+
|
|
28
|
+
## Structured request bodies
|
|
29
|
+
|
|
30
|
+
`ctx.req.text()` and `ctx.req.json()` read a bounded request body. JSON can be decoded generically or against an explicitly compiled schema. Repeated schema reads are deterministic within one request.
|
|
31
|
+
|
|
32
|
+
The schema example decodes one body twice and proves that the request-local decoded value is reused:
|
|
33
|
+
|
|
34
|
+
<!-- pulse-doc-source: examples/02-request-schema/src/index.ts -->
|
|
35
|
+
```ts
|
|
36
|
+
import { Pulse } from '@pulse-compute/pulse'
|
|
37
|
+
import type { CreateUserInput, CreateUserOutput } from './schemas.js'
|
|
38
|
+
|
|
39
|
+
const app = new Pulse({ auto: true })
|
|
40
|
+
|
|
41
|
+
app.post('/users', async (ctx) => {
|
|
42
|
+
const first = await ctx.req.json<CreateUserInput>('app.CreateUserInput')
|
|
43
|
+
const second = await ctx.req.json<CreateUserInput>('app.CreateUserInput')
|
|
44
|
+
const output: CreateUserOutput = {
|
|
45
|
+
id: 7,
|
|
46
|
+
name: first.name,
|
|
47
|
+
active: first.active,
|
|
48
|
+
sameReference: first === second,
|
|
49
|
+
}
|
|
50
|
+
return ctx.json(output, { status: 201, schema: 'app.CreateUserOutput' })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export default app
|
|
54
|
+
```
|
|
55
|
+
<!-- /pulse-doc-source -->
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pulse test examples/02-request-schema --case valid-user --json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Structured body limits are configured through `schemas.maxBytes`, `dev.maxBodyBytes`, or a test case’s `maxBodyBytes`, depending on the read path. Oversized or invalid input fails before unbounded materialization with diagnostics such as:
|
|
62
|
+
|
|
63
|
+
- [`PULSE_REQUEST_BODY_TOO_LARGE`](../reference/diagnostics.md#pulse-request-body-too-large);
|
|
64
|
+
- [`PULSE_BODY_TOO_LARGE`](../reference/diagnostics.md#pulse-body-too-large);
|
|
65
|
+
- [`PULSE_BODY_DECODE`](../reference/diagnostics.md#pulse-body-decode);
|
|
66
|
+
- [`PULSE_SCHEMA_DECODE`](../reference/diagnostics.md#pulse-schema-decode).
|
|
67
|
+
|
|
68
|
+
A successful schema decode returns a normalized, deeply immutable value.
|
|
69
|
+
Repeated request reads with the same schema ID reuse that request-local decoded
|
|
70
|
+
value. Generic JSON is also bounded, but it does not gain a typed schema
|
|
71
|
+
contract.
|
|
72
|
+
|
|
73
|
+
## Structured fetch responses
|
|
74
|
+
|
|
75
|
+
A fetch response can be inspected through normalized status, headers, `text()`, and `json<T>()`. Once application code asks for text or JSON, the body is treated as a bounded structured value governed by decoding and size policy.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const user = await ctx.fetch('https://api.example.test/user').json<{ id: number; name: string }>()
|
|
79
|
+
return ctx.json({ found: true, user })
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The application receives portable data, not a provider SDK response object.
|
|
83
|
+
|
|
84
|
+
Schema-bound fetched JSON uses the same codec and content-type policy as
|
|
85
|
+
schema-bound request JSON. A read consumes the response into a structured
|
|
86
|
+
projection for the current request; it does not expose a reusable provider
|
|
87
|
+
stream.
|
|
88
|
+
|
|
89
|
+
## Opaque response pass-through
|
|
90
|
+
|
|
91
|
+
Some responses should cross Pulse without being copied, decoded, or exposed to userland—archives, media, streaming responses, and provider-owned hold responses are examples. Return the fetch response directly:
|
|
92
|
+
|
|
93
|
+
<!-- pulse-doc-source: examples/07-opaque-proxy/src/index.ts -->
|
|
94
|
+
```ts
|
|
95
|
+
import { Pulse } from '@pulse-compute/pulse'
|
|
96
|
+
|
|
97
|
+
const app = new Pulse({ auto: true })
|
|
98
|
+
|
|
99
|
+
app.get('/archive', async (ctx) => {
|
|
100
|
+
return ctx.fetch('https://assets.example.com/archive.bin')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
export default app
|
|
104
|
+
```
|
|
105
|
+
<!-- /pulse-doc-source -->
|
|
106
|
+
|
|
107
|
+
<!-- pulse-doc-run {"args":["inspect","examples/07-opaque-proxy","--json"],"display":"pulse inspect examples/07-opaque-proxy --json"} -->
|
|
108
|
+
```bash
|
|
109
|
+
pulse inspect examples/07-opaque-proxy --json
|
|
110
|
+
```
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"status": "ok",
|
|
114
|
+
"provider": {
|
|
115
|
+
"id": "fastly"
|
|
116
|
+
},
|
|
117
|
+
"compiler": {
|
|
118
|
+
"effectCount": 1,
|
|
119
|
+
"continuationCount": 1,
|
|
120
|
+
"opaqueReturnCount": 1,
|
|
121
|
+
"providerLowering": {
|
|
122
|
+
"requirements": [
|
|
123
|
+
"fetch",
|
|
124
|
+
"opaque.pass-through"
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Opaque pass-through preserves host ownership. Pulse may carry status and headers needed to complete the response, but application code cannot inspect chunks, decode the body, concatenate it, or retain it beyond the request lifecycle.
|
|
132
|
+
|
|
133
|
+
Attempting to inspect an opaque body fails with [`PULSE_OPAQUE_BODY_INSPECTION`](../reference/diagnostics.md#pulse-opaque-body-inspection). A missing or already-consumed structured body can fail with [`PULSE_BODY_UNAVAILABLE`](../reference/diagnostics.md#pulse-body-unavailable).
|
|
134
|
+
|
|
135
|
+
## Why the distinction matters
|
|
136
|
+
|
|
137
|
+
The two body classes have different guarantees:
|
|
138
|
+
|
|
139
|
+
| Property | Structured | Opaque |
|
|
140
|
+
|---|---:|---:|
|
|
141
|
+
| Application can read text/JSON | Yes, within limits | No |
|
|
142
|
+
| Application can construct a replacement body | Yes, from supported values | No |
|
|
143
|
+
| Provider object enters userland | No | No |
|
|
144
|
+
| Body may remain host-owned | No | Yes |
|
|
145
|
+
| Suitable for binary/stream pass-through | No | Yes |
|
|
146
|
+
| Userland chunk iteration or transform | No | No |
|
|
147
|
+
|
|
148
|
+
Pulse does not infer that a body is safe to inspect merely because one provider could expose it. The same canonical source must retain equivalent meaning across supported providers.
|
|
149
|
+
|
|
150
|
+
## Schema encoding
|
|
151
|
+
|
|
152
|
+
`ctx.json(value, { schema: 'namespace.Type' })` validates and encodes a structured response against the compiled schema contract. Schema identifiers must be static and declared in project configuration. Failures use [`PULSE_SCHEMA_ENCODE`](../reference/diagnostics.md#pulse-schema-encode) or [`PULSE_RESPONSE_ENCODE`](../reference/diagnostics.md#pulse-response-encode).
|
|
153
|
+
|
|
154
|
+
`ctx.fetch(url, { json: value, schema: 'namespace.Type' })` applies the same
|
|
155
|
+
semantic encoding boundary to an outbound request. Pulse encodes the value
|
|
156
|
+
before provider dispatch; application code never receives the provider request
|
|
157
|
+
body object.
|
|
158
|
+
|
|
159
|
+
See [Explicit JSON schemas](../guides/json-schemas.md) for the exact request,
|
|
160
|
+
fetch, response, strict-mode, and response-case forms.
|
|
161
|
+
|
|
162
|
+
## GRIP hold responses are opaque
|
|
163
|
+
|
|
164
|
+
A package-owned `grip.hold(...)` operation returns an opaque response contract. The provider owns the hold/stream realization; canonical handler code may return it but may not inspect or transform its body. This is the same body boundary used by direct fetch pass-through.
|
|
165
|
+
|
|
166
|
+
## Not supported in the Beta
|
|
167
|
+
|
|
168
|
+
The public contract does not include:
|
|
169
|
+
|
|
170
|
+
- arbitrary binary body inspection;
|
|
171
|
+
- userland stream readers or writers;
|
|
172
|
+
- chunk iteration or transforms;
|
|
173
|
+
- buffering an opaque response into structured memory;
|
|
174
|
+
- provider-specific response objects;
|
|
175
|
+
- background consumption after the request completes.
|
|
176
|
+
|
|
177
|
+
These exclusions are listed in the
|
|
178
|
+
[Beta scope](../preview-scope.md).
|
|
179
|
+
|
|
180
|
+
## Related documentation
|
|
181
|
+
|
|
182
|
+
- [Canonical API](../../API.md)
|
|
183
|
+
- [Explicit JSON schemas](../guides/json-schemas.md)
|
|
184
|
+
- [Fetching and composing data](../guides/fetching-and-composition.md)
|
|
185
|
+
- [Effects and continuations](./effects-and-continuations.md)
|
|
186
|
+
- [Project configuration](../reference/project-config.md)
|