@pulse-compute/runtime 1.0.0-beta.4 → 1.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/API.md +52 -16
- package/docs/guides/routing.md +71 -1
- package/docs/preview-scope.md +17 -8
- package/package.json +2 -2
- package/src/host.d.ts +75 -0
- package/src/host.js +5 -0
- package/src/index.d.ts +25 -2
- package/src/internal/body.js +11 -2
- package/src/internal/context.js +66 -7
- package/src/internal/effect-adapter.js +57 -9
- package/src/internal/errors.js +29 -1
- package/src/internal/fetch.js +3 -2
- package/src/internal/incoming-body.js +68 -0
- package/src/internal/package-runtime.js +16 -1
- package/src/internal/redaction.js +14 -3
- package/src/internal/request-budget.js +103 -0
- package/src/internal/response.js +32 -5
- package/src/internal/router.js +86 -55
- package/src/internal/schema.js +20 -5
- package/src/internal/time.js +41 -0
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
> **Supported entry points:** `@pulse-compute/runtime`<br>
|
|
8
8
|
> **Stability:** Supported application authoring and execution contract.<br>
|
|
9
9
|
> **npm:** [`@pulse-compute/runtime`](https://www.npmjs.com/package/@pulse-compute/runtime)<br>
|
|
10
|
-
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.
|
|
10
|
+
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.6/packages/runtime/)
|
|
11
11
|
>
|
|
12
|
-
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.
|
|
12
|
+
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.6` package policy.
|
|
13
13
|
<!-- pulse-package-status:end -->
|
|
14
14
|
|
|
15
15
|
`@pulse-compute/runtime` is the low-level, provider-neutral Pulse application contract.
|
package/docs/API.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Pulse runtime contract
|
|
2
2
|
|
|
3
3
|
For source eligibility, use [Managed handler TypeScript and
|
|
4
|
-
JavaScript](https://pulsecompute.io/v1.0.0-beta.
|
|
4
|
+
JavaScript](https://pulsecompute.io/v1.0.0-beta.6/reference/handler-authoring/). For provider and target
|
|
5
5
|
differences, use the [compatibility
|
|
6
|
-
matrix](https://pulsecompute.io/v1.0.0-beta.
|
|
6
|
+
matrix](https://pulsecompute.io/v1.0.0-beta.6/reference/compatibility-matrix/). For CLI and runtime failures,
|
|
7
7
|
use the stable codes in the [diagnostics
|
|
8
|
-
reference](https://pulsecompute.io/v1.0.0-beta.
|
|
8
|
+
reference](https://pulsecompute.io/v1.0.0-beta.6/reference/diagnostics/).
|
|
9
9
|
|
|
10
10
|
This document describes the provider-neutral TypeScript application contract compiled by Pulse. Low-level authoring types and the static `Router` come from `@pulse-compute/runtime`. The public `@pulse-compute/pulse` package owns the conventional `Pulse` application root, deferred project configuration, and schema declarations, while `@pulse-compute/cli` owns workspace orchestration.
|
|
11
11
|
|
|
12
12
|
`@pulse-compute/runtime` is the low-level portable application surface and
|
|
13
13
|
`@pulse-compute/pulse` is the conventional project surface. Both belong to the
|
|
14
|
-
|
|
14
|
+
19-package public release catalog. Native and JavaScript execution remain
|
|
15
15
|
explicitly selected targets over the same canonical runtime contract.
|
|
16
16
|
|
|
17
17
|
## Context at a glance
|
|
@@ -25,9 +25,11 @@ process, provider SDK, or global network surface behind it.
|
|
|
25
25
|
| [`ctx.req.header(name)`](#request-metadata-and-headers) | HTTP handlers and middleware | Case-insensitive first-value header lookup. |
|
|
26
26
|
| [`ctx.param(name)`](#ctxparam) | Matched route handlers | Named parameters from the static route pattern. |
|
|
27
27
|
| [`ctx.state`](#ctxstate) | HTTP and event handlers | Invocation-local string state shared across one execution. |
|
|
28
|
+
| [`ctx.time.now()`](#ctxtime) | HTTP and event handlers | Provider wall-clock sample with matching Unix milliseconds and UTC text. |
|
|
28
29
|
| [`ctx.fetch`](#ctxfetch) | HTTP and event handlers | Explicit outbound HTTP effect and structured or opaque response ownership. |
|
|
29
30
|
| [`ctx.parallel`](#ctxparallel) | HTTP and event handlers | Statically keyed concurrent Pulse effects. |
|
|
30
31
|
| [`ctx.encodeJson`](#ctxencodejson) | HTTP and event handlers | Synchronous schema-bound, size-limited JSON text. |
|
|
32
|
+
| [`ctx.decodeJson`](#ctxdecodejson) | HTTP and event handlers | Synchronous schema-bound decode of bounded application text. |
|
|
31
33
|
| [`ctx.emit`](#ctxemit) | HTTP and event handlers | One-way, schema-bound event acceptance effect. |
|
|
32
34
|
| [`ctx.log`](#ctxlog) | HTTP and event handlers | Synchronous thresholded logging. |
|
|
33
35
|
| [`ctx.config`, `ctx.secret`](#config-and-secrets) | HTTP and event handlers | Explicit configured binding reads. |
|
|
@@ -50,7 +52,7 @@ Every managed handler is async-shaped:
|
|
|
50
52
|
type Handler = (ctx: PulseContext) => Promise<PulseResult | PulseFetchResponse>
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
For native targets the compiler erases the async wrapper. Awaited Pulse effects lower into the existing explicit effect and continuation state machine; no Promise runtime or Asyncify transform is linked. Awaiting a proven synchronous `ctx` expression is redundant and may warn, while arbitrary non-Pulse awaits mark the native eligibility boundary. The canonical [handler authoring reference](https://pulsecompute.io/v1.0.0-beta.
|
|
55
|
+
For native targets the compiler erases the async wrapper. Awaited Pulse effects lower into the existing explicit effect and continuation state machine; no Promise runtime or Asyncify transform is linked. Awaiting a proven synchronous `ctx` expression is redundant and may warn, while arbitrary non-Pulse awaits mark the native eligibility boundary. The canonical [handler authoring reference](https://pulsecompute.io/v1.0.0-beta.6/reference/handler-authoring/) defines the static language subset; the [compatibility matrix](https://pulsecompute.io/v1.0.0-beta.6/reference/compatibility-matrix/) owns the tested four-mode claims.
|
|
54
56
|
|
|
55
57
|
## Static `Router`
|
|
56
58
|
|
|
@@ -138,9 +140,9 @@ eligibility. Fastly targets fail closed because no event ingress/emit adapter is
|
|
|
138
140
|
claimed. There is no public event injection command, no event-aware development
|
|
139
141
|
listener, no HTTP/GRIP translation, and no automatic target fallback.
|
|
140
142
|
|
|
141
|
-
The [static events guide](https://pulsecompute.io/v1.0.0-beta.
|
|
143
|
+
The [static events guide](https://pulsecompute.io/v1.0.0-beta.6/guides/events/) owns the complete frame,
|
|
142
144
|
queue, target-eligibility, diagnostic, and Native-extension contract. The
|
|
143
|
-
source-bound [event example](https://pulsecompute.io/v1.0.0-beta.
|
|
145
|
+
source-bound [event example](https://pulsecompute.io/v1.0.0-beta.6/examples/11-events/) runs the same mixed project
|
|
144
146
|
on Node JavaScript and Node Native.
|
|
145
147
|
|
|
146
148
|
## `ctx.req`
|
|
@@ -152,6 +154,8 @@ interface PulseRequest {
|
|
|
152
154
|
readonly path: string
|
|
153
155
|
readonly headers: readonly [string, string][]
|
|
154
156
|
header(name: string): string | undefined
|
|
157
|
+
body(): PulseIncomingBody
|
|
158
|
+
readTextChunk(): PulseEffect<{ readonly done: boolean; readonly text: string }>
|
|
155
159
|
text(): PulseEffect<string>
|
|
156
160
|
json<T = unknown>(schemaId?: string): PulseEffect<T>
|
|
157
161
|
}
|
|
@@ -159,6 +163,15 @@ interface PulseRequest {
|
|
|
159
163
|
|
|
160
164
|
Structured request bodies are bounded runtime-owned snapshots. Repeated `text()` and `json()` reads are memoized immutable transforms.
|
|
161
165
|
|
|
166
|
+
`body()` is an opaque incoming handle under the Node `bodyForwarding` opt-in;
|
|
167
|
+
it grants no reader or binary inspection. `readTextChunk()` belongs only to the
|
|
168
|
+
experimental Node `bodyTransform` opt-in, with generated output and a finite
|
|
169
|
+
deadline. It excludes forwarding and structured body reads. Both Fastly targets
|
|
170
|
+
reject these capabilities. The [bodies guide](https://pulsecompute.io/v1.0.0-beta.6/concepts/bodies/) owns the
|
|
171
|
+
byte/read/write limits and separates installed output qualification from pending
|
|
172
|
+
installed transform qualification. The production `/server` launcher remains
|
|
173
|
+
qualified for finite HTTP, as described in [Node deployment](https://pulsecompute.io/v1.0.0-beta.6/guides/deploying-node/).
|
|
174
|
+
|
|
162
175
|
### Request metadata and headers
|
|
163
176
|
|
|
164
177
|
```ts
|
|
@@ -382,6 +395,16 @@ return ctx.json(output, {
|
|
|
382
395
|
})
|
|
383
396
|
```
|
|
384
397
|
|
|
398
|
+
## `ctx.time`
|
|
399
|
+
|
|
400
|
+
`await ctx.time.now()` returns one provider-owned wall-clock sample. On success,
|
|
401
|
+
`status` is `'ok'`, `unixEpochMs` is integer Unix milliseconds and `iso8601` is
|
|
402
|
+
its matching UTC string. A failed sample has `status: 'failed'` and reason
|
|
403
|
+
`'unavailable'` or `'invalid-clock'`. Direct awaited calls and keyed parallel
|
|
404
|
+
members are supported on Node/Fastly Native and JavaScript. Wall time can regress
|
|
405
|
+
and is separate from monotonic deadlines and exact commit timestamps. See the
|
|
406
|
+
[complete result contract](https://pulsecompute.io/v1.0.0-beta.6/packages/runtime/#wall-time).
|
|
407
|
+
|
|
385
408
|
## Config and secrets
|
|
386
409
|
|
|
387
410
|
```ts
|
|
@@ -453,7 +476,20 @@ projects the value, then returns detached JSON text bounded by `schemas.maxBytes
|
|
|
453
476
|
in UTF-8 bytes. It does not create a response or dispatch an effect. Invalid
|
|
454
477
|
values and oversized text fail before subsequent writes. Declaration order and
|
|
455
478
|
array order are preserved; cross-target parity is semantic, not a universal
|
|
456
|
-
canonical-byte format. See [application-owned encoding](https://pulsecompute.io/v1.0.0-beta.
|
|
479
|
+
canonical-byte format. See [application-owned encoding](https://pulsecompute.io/v1.0.0-beta.6/guides/json-schemas/#encode-application-owned-text).
|
|
480
|
+
|
|
481
|
+
## `ctx.decodeJson`
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
const candidate = ctx.decodeJson<Candidate>(stored.text, 'app.Candidate')
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Requires a literal registered schema even in non-strict mode. The input must
|
|
488
|
+
be a string within `schemas.maxBytes` UTF-8 bytes. Decoding returns a detached,
|
|
489
|
+
deeply immutable value with declared fields only. It performs no effect and
|
|
490
|
+
does not apply HTTP content-type policy. Keep the original text for hashing,
|
|
491
|
+
verification and retries; decoding does not establish byte canonicalization or
|
|
492
|
+
storage acceptance. See [application text decoding](https://pulsecompute.io/v1.0.0-beta.6/guides/json-schemas/#decode-application-owned-text).
|
|
457
493
|
|
|
458
494
|
## Explicit JSON schemas
|
|
459
495
|
|
|
@@ -496,12 +532,12 @@ forms lower into canonical package operations; JavaScript targets execute the
|
|
|
496
532
|
real package implementation.
|
|
497
533
|
|
|
498
534
|
Older `/pulsewasm` imports are compatibility-only and are isolated in the
|
|
499
|
-
[migration guide](https://pulsecompute.io/v1.0.0-beta.
|
|
500
|
-
[GRIP package guide](https://pulsecompute.io/v1.0.0-beta.
|
|
535
|
+
[migration guide](https://pulsecompute.io/v1.0.0-beta.6/guides/compatibility-imports/). See the
|
|
536
|
+
[GRIP package guide](https://pulsecompute.io/v1.0.0-beta.6/packages/grip/) for the complete current surface.
|
|
501
537
|
|
|
502
538
|
## Entities API
|
|
503
539
|
|
|
504
|
-
`@pulse-compute/entities` is part of the synchronized `1.0.0-beta.
|
|
540
|
+
`@pulse-compute/entities` is part of the synchronized `1.0.0-beta.6` package
|
|
505
541
|
set. The application surface has two runtime values:
|
|
506
542
|
|
|
507
543
|
```ts
|
|
@@ -563,9 +599,9 @@ Registrations and the terminal binding must use the supported static form. The
|
|
|
563
599
|
first-party JSON-RPC adapter accepts bounded JSON-RPC 2.0 request objects and
|
|
564
600
|
named params, validates declared schemas, uses stable error framing, and
|
|
565
601
|
acknowledges notifications with HTTP `204`. See the [package
|
|
566
|
-
guide](https://pulsecompute.io/v1.0.0-beta.
|
|
567
|
-
model](https://pulsecompute.io/v1.0.0-beta.
|
|
568
|
-
example](https://pulsecompute.io/v1.0.0-beta.
|
|
602
|
+
guide](https://pulsecompute.io/v1.0.0-beta.6/packages/entities/), [entity/adapter
|
|
603
|
+
model](https://pulsecompute.io/v1.0.0-beta.6/concepts/entities-and-adapters/), and [executable
|
|
604
|
+
example](https://pulsecompute.io/v1.0.0-beta.6/examples/10-entities-tools/).
|
|
569
605
|
|
|
570
606
|
## Project workflow
|
|
571
607
|
|
|
@@ -582,5 +618,5 @@ selected profile, handler entry, schema declarations, provider bindings, and
|
|
|
582
618
|
output directory through `.pulse/config.ts`. `pulse inspect` is optional
|
|
583
619
|
observability, and `pulse compile` is the advanced provider-neutral Native
|
|
584
620
|
artifact command; neither is required before `pulse build`. See the [project
|
|
585
|
-
lifecycle guide](https://pulsecompute.io/v1.0.0-beta.
|
|
586
|
-
reference](https://pulsecompute.io/v1.0.0-beta.
|
|
621
|
+
lifecycle guide](https://pulsecompute.io/v1.0.0-beta.6/guides/project-lifecycle/) and [CLI
|
|
622
|
+
reference](https://pulsecompute.io/v1.0.0-beta.6/reference/cli/).
|
package/docs/guides/routing.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Static Router authoring
|
|
2
2
|
|
|
3
|
-
Use `Router` when an application has several method/path entry points or needs compile-time middleware. It is a static authoring marker from `@pulse-compute/runtime`, not a JavaScript runtime dispatcher. The shared async/static restrictions are defined in [Managed handler TypeScript and JavaScript](https://pulsecompute.io/v1.0.0-beta.
|
|
3
|
+
Use `Router` when an application has several method/path entry points or needs compile-time middleware. It is a static authoring marker from `@pulse-compute/runtime`, not a JavaScript runtime dispatcher. The shared async/static restrictions are defined in [Managed handler TypeScript and JavaScript](https://pulsecompute.io/v1.0.0-beta.6/reference/handler-authoring/); target claims live in the [compatibility matrix](https://pulsecompute.io/v1.0.0-beta.6/reference/compatibility-matrix/).
|
|
4
4
|
|
|
5
5
|
<!-- pulse-doc-source: examples/09-router-lowering/src/index.ts -->
|
|
6
6
|
```ts
|
|
@@ -105,6 +105,43 @@ acceptance only; they do not implement Catalog persistence or authorization.
|
|
|
105
105
|
node wasm/scripts/run-wasm-tests.cjs --task catalog-router-parity --no-report
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
## Selected groups
|
|
109
|
+
|
|
110
|
+
Mount a group once after earlier selectors have set request-local state:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
app.mount('/api', collection, { state: 'read-family', equals: 'collection' })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The optional third argument is an inline object containing exactly `state` and
|
|
117
|
+
`equals`, both string literals. At this mount's ordered position, the request
|
|
118
|
+
path must match and `ctx.state.get('read-family') === 'collection'` must hold.
|
|
119
|
+
An absent or unequal value skips every child owner and effect directly to the
|
|
120
|
+
parent continuation. Selection is not authorization; keep authorization in its
|
|
121
|
+
existing owner.
|
|
122
|
+
|
|
123
|
+
A match enters the child once. Child `next()` still advances to the next sibling,
|
|
124
|
+
and exhaustion joins the parent continuation once. Terminal responses and ordered
|
|
125
|
+
local/parent error handling keep their existing semantics. A suspended effect
|
|
126
|
+
resumes inside the child; changing selection state there does not reevaluate the
|
|
127
|
+
mount. Method, path and parameter scoping are unchanged. An empty selected child
|
|
128
|
+
continues immediately.
|
|
129
|
+
|
|
130
|
+
This form is supported by live JavaScript and canonical Native compilation.
|
|
131
|
+
Eligibility does not accept callbacks, calls, getters, spreads, computed keys,
|
|
132
|
+
descriptor references or dynamic values. Unsupported source receives
|
|
133
|
+
`PULSEWASM_UNSUPPORTED_MOUNT_ELIGIBILITY`; historical table/harness emitters also
|
|
134
|
+
reject selected mounts rather than ignoring them. Two-argument mounts are
|
|
135
|
+
unchanged. The group remains one placement with one existing parent continuation.
|
|
136
|
+
|
|
137
|
+
The `router-selected-groups` task checks exclusion, suspension after a state
|
|
138
|
+
change, terminal/error paths, nested parameters and one common tail on live
|
|
139
|
+
JavaScript and compiled Node/Fastly Native (Fastly uses its local ABI mock host).
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
node wasm/scripts/run-wasm-tests.cjs --task router-selected-groups --no-report
|
|
143
|
+
```
|
|
144
|
+
|
|
108
145
|
## `next()` is a terminal transfer
|
|
109
146
|
|
|
110
147
|
`next()` is not an onion-style callback. It is a compiler-visible control transfer:
|
|
@@ -146,6 +183,39 @@ app.error(async (error, ctx, next) => {
|
|
|
146
183
|
|
|
147
184
|
If the normal lane is exhausted, Pulse returns `404 Not Found`. If the error lane is exhausted, Pulse returns `500 Internal Server Error`.
|
|
148
185
|
|
|
186
|
+
Request UTF-8 failures, schema data failures and JWT validation failures enter the next registered
|
|
187
|
+
error handler. Branch on `error.code`; the portable contract does not require
|
|
188
|
+
identical messages, stacks, causes or detail fields across providers.
|
|
189
|
+
|
|
190
|
+
| Boundary | Portable error codes |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| Request text encoding | `PULSE_REQUEST_BODY_INVALID_UTF8` (Node Native, Node JavaScript, Fastly Native) |
|
|
193
|
+
| Schema data | `PULSE_SCHEMA_DECODE`, `PULSE_SCHEMA_ENCODE`, `PULSE_SCHEMA_JSON_MALFORMED`, `PULSE_SCHEMA_CONTENT_TYPE`, `PULSE_BODY_TOO_LARGE` |
|
|
194
|
+
| JWT input and verification | `PULSE_JWT_TOKEN_REQUIRED`, `PULSE_JWT_BEARER_INVALID`, `PULSE_JWT_MALFORMED`, `PULSE_JWT_LIMIT_EXCEEDED`, `PULSE_JWT_ALGORITHM_NOT_ALLOWED`, `PULSE_JWT_KEY_INVALID`, `PULSE_JWT_SIGNATURE_INVALID` |
|
|
195
|
+
| JWT claims | `PULSE_JWT_CLOCK_INVALID`, `PULSE_JWT_CLAIMS_INVALID`, `PULSE_JWT_CLAIMS_SCHEMA_INVALID` |
|
|
196
|
+
|
|
197
|
+
These admitted `error.code` values remain stable through redaction, including
|
|
198
|
+
when a secret, KV key or array index overlaps their spelling. Messages, stacks,
|
|
199
|
+
causes, details and arbitrary provider error codes remain subject to redaction.
|
|
200
|
+
This preserves the existing recovery catalog; it does not admit new failures.
|
|
201
|
+
|
|
202
|
+
Recovery moves forward in registration order, including through mounted
|
|
203
|
+
routers. A failed handler never resumes. An error handler can return a response,
|
|
204
|
+
forward with `return next(error)`, or clear the error lane with `return next()`.
|
|
205
|
+
A data failure inside an error handler transfers to a later error handler.
|
|
206
|
+
|
|
207
|
+
Already-started members of a failed effect group settle before application
|
|
208
|
+
recovery. Pulse does not retry them, undo completed writes, or imply that a
|
|
209
|
+
failed invocation had no external effects. Conditional KV results such as
|
|
210
|
+
`conflict`, `not-stored` and `unknown` remain ordinary outcomes for the handler to
|
|
211
|
+
inspect. Their meaning does not change when an error handler is registered.
|
|
212
|
+
|
|
213
|
+
Cancellation ends the invocation without an application response. On Native,
|
|
214
|
+
traps, provider protocol failures and unavailable capabilities remain terminal; they
|
|
215
|
+
do not acquire recovery or rollback guarantees. Native authoring still does not
|
|
216
|
+
admit arbitrary `throw` or `try`/`catch`. JavaScript retains its existing
|
|
217
|
+
`PulseUnhandledError` containment for unexpected handler failures.
|
|
218
|
+
|
|
149
219
|
## Middleware scope and effects
|
|
150
220
|
|
|
151
221
|
A path-scoped `use('/api', handler)` applies only to that subtree. Middleware declared on a mounted child Router is naturally limited to the mounted subtree.
|
package/docs/preview-scope.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Beta scope
|
|
2
2
|
|
|
3
|
-
Pulse `1.0.0-beta.
|
|
3
|
+
Pulse `1.0.0-beta.6` is a Beta of one provider-neutral application contract
|
|
4
4
|
with explicit Native and JavaScript execution targets. The intended 1.0
|
|
5
5
|
surface is present, but deliberate corrections may still occur before the
|
|
6
6
|
stable `1.0.0` release.
|
|
@@ -18,6 +18,8 @@ targets.
|
|
|
18
18
|
middleware, GET/HEAD/POST/PUT/PATCH/DELETE routes, exact paths, named parameters, trailing
|
|
19
19
|
wildcards, and acyclic mounts.
|
|
20
20
|
- Basic branching and structured object, array, and scalar manipulation.
|
|
21
|
+
- Literal-capped pure `for` loops and zero-argument string `.trim()` across
|
|
22
|
+
Native and JavaScript; see [bounded application values](https://pulsecompute.io/v1.0.0-beta.6/concepts/compilation-and-lowering/#bounded-application-values).
|
|
21
23
|
- Request method, URL, path, headers, text, and JSON access.
|
|
22
24
|
- Bounded, memoized structured body decoding.
|
|
23
25
|
- Explicit TypeScript JSON schema declarations and literal schema IDs.
|
|
@@ -75,6 +77,11 @@ JavaScript-selected project into a Native one.
|
|
|
75
77
|
|
|
76
78
|
## Deliberately unsupported
|
|
77
79
|
|
|
80
|
+
- Unbounded loops, arbitrary callback transformations and Native helper shapes
|
|
81
|
+
outside the supported bounded subset. Effectful loops are limited to the
|
|
82
|
+
experimental finite Node output/transform forms in the [bodies guide](https://pulsecompute.io/v1.0.0-beta.6/concepts/bodies/);
|
|
83
|
+
other loop bodies must stay within the admitted pure forms.
|
|
84
|
+
|
|
78
85
|
- Automatic fallback from Native lowering to JavaScript execution.
|
|
79
86
|
- Declaring general target availability without satisfying every declared
|
|
80
87
|
full-target-support gate.
|
|
@@ -88,7 +95,9 @@ JavaScript-selected project into a Native one.
|
|
|
88
95
|
- Automatic discovery of arbitrary TypeScript types.
|
|
89
96
|
- Dynamic schema IDs.
|
|
90
97
|
- Arbitrary binary body inspection or mutation.
|
|
91
|
-
-
|
|
98
|
+
- General userland chunk iteration, transform streams, or manual backpressure.
|
|
99
|
+
Experimental finite Node UTF-8 output/transforms are explicit bounded opt-ins,
|
|
100
|
+
not general stream support or production-launcher qualification.
|
|
92
101
|
- Background tasks and work that outlives the request.
|
|
93
102
|
- Raw TCP or UDP sockets.
|
|
94
103
|
- Dynamic GRIP channel, framing-option, or message shapes under Native
|
|
@@ -106,13 +115,13 @@ JavaScript-selected project into a Native one.
|
|
|
106
115
|
## Compatibility authority
|
|
107
116
|
|
|
108
117
|
The single source-form, provider-binding, artifact, and deployment-boundary
|
|
109
|
-
table is [Provider and target compatibility](https://pulsecompute.io/v1.0.0-beta.
|
|
118
|
+
table is [Provider and target compatibility](https://pulsecompute.io/v1.0.0-beta.6/reference/compatibility-matrix/).
|
|
110
119
|
It uses one public target order—Node JavaScript, Fastly JavaScript, Node Native,
|
|
111
120
|
and Fastly Native—and links every row to a focused proof or canonical contract.
|
|
112
121
|
|
|
113
122
|
The exact portable language subset and the JavaScript-only Native eligibility
|
|
114
123
|
boundaries are defined in
|
|
115
|
-
[Managed handler TypeScript and JavaScript](https://pulsecompute.io/v1.0.0-beta.
|
|
124
|
+
[Managed handler TypeScript and JavaScript](https://pulsecompute.io/v1.0.0-beta.6/reference/handler-authoring/).
|
|
116
125
|
|
|
117
126
|
## Body model
|
|
118
127
|
|
|
@@ -136,7 +145,7 @@ pass it through → opaque handle
|
|
|
136
145
|
- Package names, release artifacts, and published versions are immutable once
|
|
137
146
|
released.
|
|
138
147
|
|
|
139
|
-
The
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
The release manifest explicitly selects the npm `latest` dist-tag while package
|
|
149
|
+
versions retain the Beta prerelease suffix. The npm tag and Git branch are
|
|
150
|
+
separate names. Publication remains a distinct human-authorized action; this
|
|
151
|
+
support cleanup does not change that policy.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/runtime",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^22.14.0 || ^24.0.0"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
42
42
|
"directory": "packages/runtime"
|
|
43
43
|
},
|
|
44
|
-
"homepage": "https://pulsecompute.io/v1.0.0-beta.
|
|
44
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.6/packages/runtime/",
|
|
45
45
|
"bugs": {
|
|
46
46
|
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
47
47
|
}
|
package/src/host.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Handler,
|
|
3
|
+
PulseTimeResult,
|
|
3
4
|
PulseEffect,
|
|
4
5
|
PulseKvGeneration, PulseKvVersionedResult, PulseKvConditionalResult,
|
|
5
6
|
PulseFetchResponse,
|
|
@@ -16,6 +17,8 @@ export interface PulseJavascriptEffectHostExecution {
|
|
|
16
17
|
readonly application?: unknown;
|
|
17
18
|
readonly signal?: AbortSignal;
|
|
18
19
|
readonly deadlineMonotonicMs?: number;
|
|
20
|
+
/** Host-only request authority; package transports recheck before dispatch. */
|
|
21
|
+
readonly requestBudget?: PulseRequestBudget;
|
|
19
22
|
/** Adds execution-owned sensitive text to runtime redaction before provider work continues. */
|
|
20
23
|
registerRedactionValue(value: string | Uint8Array): void;
|
|
21
24
|
/** Applies one project-owned schema codec directly to an in-memory semantic value. */
|
|
@@ -112,6 +115,7 @@ export interface PulseRuntimeHostKvNamespace<T = unknown> {
|
|
|
112
115
|
/** Compatibility injection shape retained while providers move to one effect adapter. */
|
|
113
116
|
export interface PulseRuntimeHostCapabilities {
|
|
114
117
|
prepareConditionalKv?: PulseConditionalKvPreparation;
|
|
118
|
+
time?(execution?: PulseJavascriptEffectHostExecution): PulseTimeResult | Promise<PulseTimeResult>;
|
|
115
119
|
fetch?(
|
|
116
120
|
url: string,
|
|
117
121
|
init?: unknown,
|
|
@@ -173,6 +177,8 @@ export interface PulseJavascriptEffectSummary {
|
|
|
173
177
|
|
|
174
178
|
/** Maintainer bridge used by the managed Router lifecycle and focused adapter tests. */
|
|
175
179
|
export interface PulseJavascriptEffectExecution {
|
|
180
|
+
/** Host/runtime admission fence; rejects queued provider dispatch after an ownership conflict. */
|
|
181
|
+
invalidateAdmission(error: unknown): void;
|
|
176
182
|
/** Execution-owned local projection. It participates in lifecycle containment but is not parallel-eligible. */
|
|
177
183
|
local<T = unknown>(
|
|
178
184
|
kind: string,
|
|
@@ -199,7 +205,48 @@ export interface PulseJavascriptEffectExecution {
|
|
|
199
205
|
close(): Promise<void>;
|
|
200
206
|
}
|
|
201
207
|
|
|
208
|
+
export interface PulseRequestBudget {
|
|
209
|
+
readonly signal: AbortSignal;
|
|
210
|
+
readonly deadlineMonotonicMs?: number;
|
|
211
|
+
readonly clock: PulseKvClock;
|
|
212
|
+
check(): void;
|
|
213
|
+
onAbort(callback: () => void): () => void;
|
|
214
|
+
remainingMs(): number;
|
|
215
|
+
race<T>(value: T | PromiseLike<T>): Promise<T>;
|
|
216
|
+
close(): void;
|
|
217
|
+
}
|
|
218
|
+
export declare function normalizeRequestDuration(value?: number): number | undefined;
|
|
219
|
+
export declare function createRequestBudget(options?: {
|
|
220
|
+
maxDurationMs?: number;
|
|
221
|
+
requestBudget?: PulseRequestBudget;
|
|
222
|
+
requestClock?: PulseKvClock;
|
|
223
|
+
signal?: AbortSignal;
|
|
224
|
+
requestSignal?: AbortSignal;
|
|
225
|
+
}): PulseRequestBudget;
|
|
226
|
+
|
|
227
|
+
export interface PulseGeneratedOutputExecution {
|
|
228
|
+
readonly started: boolean;
|
|
229
|
+
readonly finished: boolean;
|
|
230
|
+
assertEffect(): void;
|
|
231
|
+
dispatch(effect: Readonly<Record<string, unknown>>): Promise<unknown>;
|
|
232
|
+
bindInput(input: PulseIncomingBodyOwnership | undefined): void;
|
|
233
|
+
close(factory?: (options: import('./index').PulseResponseOptions) => unknown): any;
|
|
234
|
+
validateResult(result: unknown): void;
|
|
235
|
+
finish(): Promise<void>;
|
|
236
|
+
cancel(reason?: unknown): void;
|
|
237
|
+
dispose(): void;
|
|
238
|
+
}
|
|
202
239
|
export interface PulseRuntimeExecutionOptions {
|
|
240
|
+
/** Provider-owned output authority; unavailable to application code. */
|
|
241
|
+
readonly outputExecution?: PulseGeneratedOutputExecution;
|
|
242
|
+
/** Provider-owned incoming stream controller; never an application capability. */
|
|
243
|
+
readonly incomingBody?: PulseIncomingBodyOwnership;
|
|
244
|
+
/** Provider-owned total managed request budget, 1..30000 ms. Omitted preserves existing behavior. */
|
|
245
|
+
readonly maxDurationMs?: number;
|
|
246
|
+
/** Shared host budget; nested adapters must not restart or close an inherited budget. */
|
|
247
|
+
readonly requestBudget?: PulseRequestBudget;
|
|
248
|
+
/** Monotonic host clock/scheduler, never application wall time. */
|
|
249
|
+
readonly requestClock?: PulseKvClock;
|
|
203
250
|
/** Host-only monotonic clock/scheduler injection for deterministic lifecycle evidence. */
|
|
204
251
|
readonly kvClock?: PulseKvClock;
|
|
205
252
|
/** Host monotonic deadline; conditional KV uses the earlier of this and 10 seconds. */
|
|
@@ -418,3 +465,31 @@ export declare function admitConditionalKv(input: Readonly<Record<string, unknow
|
|
|
418
465
|
export declare function normalizeConditionalKvResult(effect: Readonly<{ kind: string }>, value: unknown, options?: PulseBindingValueLimits): PulseKvVersionedResult<unknown> | PulseKvConditionalResult;
|
|
419
466
|
export declare function executeConditionalKv(effect: Readonly<Record<string, unknown>>, prepare: PulseConditionalKvPreparation, execution?: PulseConditionalKvExecution, options?: PulseRuntimeExecutionOptions): Promise<PulseKvVersionedResult<unknown> | PulseKvConditionalResult>;
|
|
420
467
|
export declare function registerKvRedactions(effect: Readonly<Record<string, unknown>>, register?: (value: string) => void): void;
|
|
468
|
+
/** Whether a data failure can transfer to the next Router error handler. */
|
|
469
|
+
export declare function isApplicationError(error: unknown): boolean;
|
|
470
|
+
|
|
471
|
+
/** Host-only wall-clock result contract; the callback samples integer Unix milliseconds. */
|
|
472
|
+
export declare const WALL_TIME_MAX_MS: 253402300799999;
|
|
473
|
+
export declare function readWallTime(clock?: (() => number) | null): PulseTimeResult;
|
|
474
|
+
export declare function normalizeTimeResult(value: unknown): PulseTimeResult;
|
|
475
|
+
|
|
476
|
+
/** Host-only lifecycle authority for one incoming request body. */
|
|
477
|
+
export interface PulseIncomingBodyOwnership {
|
|
478
|
+
marker(): import('./index').PulseIncomingBody;
|
|
479
|
+
bindInvalidation(callback: (error: unknown) => void): void;
|
|
480
|
+
structured(): void;
|
|
481
|
+
readTextChunk(budget: PulseRequestBudget): Promise<{ readonly done: boolean; readonly text: string }>;
|
|
482
|
+
claim(marker: unknown, init: unknown): void;
|
|
483
|
+
forward(url: string, init: unknown, execution: PulseJavascriptEffectHostExecution): Promise<Response>;
|
|
484
|
+
close(): Promise<void>;
|
|
485
|
+
readonly failure: unknown;
|
|
486
|
+
readonly responseSignal?: AbortSignal;
|
|
487
|
+
}
|
|
488
|
+
export declare function createIncomingBodyOwnership(transport: {
|
|
489
|
+
readonly responseSignal?: AbortSignal;
|
|
490
|
+
readTextChunk?(budget: PulseRequestBudget): Promise<{ readonly done: boolean; readonly text: string }>;
|
|
491
|
+
validate(init: unknown): void;
|
|
492
|
+
forward(url: string, init: unknown, execution: PulseJavascriptEffectHostExecution): Promise<Response>;
|
|
493
|
+
cancel(reason?: unknown): unknown;
|
|
494
|
+
}): PulseIncomingBodyOwnership;
|
|
495
|
+
export declare function forwardIncomingBody(marker: unknown, url: string, init: unknown, execution: PulseJavascriptEffectHostExecution): Promise<Response>;
|
package/src/host.js
CHANGED
|
@@ -78,6 +78,11 @@ async function executeApplication(application, request, options = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
module.exports = Object.freeze({
|
|
81
|
+
createIncomingBodyOwnership: require('./internal/incoming-body.js').createIncomingBodyOwnership,
|
|
82
|
+
forwardIncomingBody: require('./internal/incoming-body.js').forwardIncomingBody,
|
|
83
|
+
...require('./internal/time.js'),
|
|
84
|
+
...require('./internal/request-budget.js'),
|
|
85
|
+
isApplicationError: require('./internal/errors.js').isApplicationError,
|
|
81
86
|
...require('./internal/conditional-kv.js'),
|
|
82
87
|
RUNTIME_HOST_API_VERSION,
|
|
83
88
|
JAVASCRIPT_EFFECT_PROTOCOL_VERSION,
|
package/src/index.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ export type HeaderPair = readonly [name: string, value: string];
|
|
|
2
2
|
|
|
3
3
|
declare const pulseEffectBrand: unique symbol;
|
|
4
4
|
declare const pulseParallelEffectBrand: unique symbol;
|
|
5
|
+
declare const incomingBodyBrand: unique symbol;
|
|
6
|
+
|
|
7
|
+
/** Single-use opaque marker, admitted only inline as a configured Node fetch body. */
|
|
8
|
+
export interface PulseIncomingBody { readonly [incomingBodyBrand]: true; }
|
|
5
9
|
|
|
6
10
|
export interface PulseEffect<T> extends Promise<T> {
|
|
7
11
|
readonly [pulseEffectBrand]: T;
|
|
@@ -23,6 +27,9 @@ export interface PulseRequest {
|
|
|
23
27
|
readonly path: string;
|
|
24
28
|
readonly headers: readonly HeaderPair[];
|
|
25
29
|
header(name: string): string | undefined;
|
|
30
|
+
body(): PulseIncomingBody;
|
|
31
|
+
/** Experimental finite UTF-8 transform input; Node bodyTransform opt-in required. */
|
|
32
|
+
readTextChunk(): PulseEffect<{ readonly done: boolean; readonly text: string }>;
|
|
26
33
|
text(): PulseEffect<string>;
|
|
27
34
|
json<T = unknown>(schemaId?: string): PulseEffect<T>;
|
|
28
35
|
}
|
|
@@ -35,7 +42,7 @@ export interface PulseFetchInitBase {
|
|
|
35
42
|
|
|
36
43
|
export type PulseFetchInit = PulseFetchInitBase & (
|
|
37
44
|
| { readonly body?: never; readonly json?: never; readonly schema?: never }
|
|
38
|
-
| { readonly body: string; readonly json?: never; readonly schema?: never }
|
|
45
|
+
| { readonly body: string | PulseIncomingBody; readonly json?: never; readonly schema?: never }
|
|
39
46
|
| { readonly body?: never; readonly json: unknown; readonly schema?: string }
|
|
40
47
|
);
|
|
41
48
|
|
|
@@ -133,10 +140,19 @@ export type PulseEmitEvent<Payload = unknown> =
|
|
|
133
140
|
| Readonly<{ schema: string; payload: Payload }>
|
|
134
141
|
| Readonly<{ schema: null; payload?: never }>;
|
|
135
142
|
|
|
143
|
+
/** One provider wall-clock sample. This is neither monotonic nor a commit timestamp. */
|
|
144
|
+
export type PulseTimeResult =
|
|
145
|
+
| { readonly status: 'ok'; readonly unixEpochMs: number; readonly iso8601: string }
|
|
146
|
+
| { readonly status: 'failed'; readonly reason: 'unavailable' | 'invalid-clock' };
|
|
147
|
+
|
|
136
148
|
/** Plane-neutral authority shared by one isolated HTTP request or event invocation. */
|
|
137
149
|
export interface PulseExecutionContext {
|
|
138
150
|
/** Validate/project a value through a literal registered schema and return bounded JSON text. */
|
|
139
151
|
encodeJson(value: unknown, schemaId: string): string;
|
|
152
|
+
/** Decode bounded application-owned JSON text through a literal registered schema. */
|
|
153
|
+
decodeJson<T = unknown>(text: string, schemaId: string): T;
|
|
154
|
+
/** Fresh sample at dispatch, UTC 1970–9999, integer Unix milliseconds. */
|
|
155
|
+
readonly time: { now(): PulseParallelEffect<PulseTimeResult> };
|
|
140
156
|
readonly state: PulseState;
|
|
141
157
|
readonly log: PulseLogger;
|
|
142
158
|
fetch(url: string, init?: PulseFetchInit): PulseFetchOperation;
|
|
@@ -157,6 +173,12 @@ export interface PulseExecutionContext {
|
|
|
157
173
|
|
|
158
174
|
/** HTTP request execution context. */
|
|
159
175
|
export interface PulseContext extends PulseExecutionContext {
|
|
176
|
+
/** Experimental Node output: opt-in, finite, request-owned UTF-8 writes. */
|
|
177
|
+
readonly output: {
|
|
178
|
+
start(options?: PulseResponseOptions): PulseEffect<void>;
|
|
179
|
+
write(text: string): PulseEffect<void>;
|
|
180
|
+
close(): PulseResult;
|
|
181
|
+
};
|
|
160
182
|
readonly req: PulseRequest;
|
|
161
183
|
json(value: unknown, descriptor?: PulseJsonResponseOptions | string): PulseResult;
|
|
162
184
|
text(value: string, options?: PulseResponseOptions): PulseResult;
|
|
@@ -209,7 +231,8 @@ export declare class Router {
|
|
|
209
231
|
put(path: string, handler: RouteHandler): this;
|
|
210
232
|
patch(path: string, handler: RouteHandler): this;
|
|
211
233
|
delete(path: string, handler: RouteHandler): this;
|
|
212
|
-
|
|
234
|
+
/** Enter the child once only when request state strictly equals the literal string; a miss skips the entire child. */
|
|
235
|
+
mount(path: string, router: Router, eligibility?: { readonly state: string; readonly equals: string }): this;
|
|
213
236
|
error(handler: RouterErrorHandler): this;
|
|
214
237
|
}
|
|
215
238
|
|
package/src/internal/body.js
CHANGED
|
@@ -200,13 +200,22 @@ function createStructuredBodyReader(owner, options = {}) {
|
|
|
200
200
|
|
|
201
201
|
function bytes() {
|
|
202
202
|
assertInspectable();
|
|
203
|
-
if (!bytesPromise)
|
|
203
|
+
if (!bytesPromise) {
|
|
204
|
+
bytesPromise = readBodyBytes(owner, maxBytes, label, signal);
|
|
205
|
+
if (options.onReadSettled) bytesPromise = bytesPromise.finally(options.onReadSettled);
|
|
206
|
+
}
|
|
204
207
|
return bytesPromise;
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
function text() {
|
|
208
211
|
assertInspectable();
|
|
209
|
-
if (!textPromise) textPromise = bytes().then((value) =>
|
|
212
|
+
if (!textPromise) textPromise = bytes().then((value) => {
|
|
213
|
+
if (label !== 'request') return new TextDecoder().decode(value);
|
|
214
|
+
try { return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(value); }
|
|
215
|
+
catch (cause) {
|
|
216
|
+
throw bodyError('PULSE_REQUEST_BODY_INVALID_UTF8', 'Pulse request text must be well-formed UTF-8.', {}, cause);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
210
219
|
return textPromise;
|
|
211
220
|
}
|
|
212
221
|
|