@pulse-compute/runtime 1.0.0-beta.1 → 1.0.0-beta.3

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 CHANGED
@@ -6,9 +6,10 @@
6
6
  > **Install directly:** Yes. Install it in every Pulse application.<br>
7
7
  > **Supported entry points:** `@pulse-compute/runtime`<br>
8
8
  > **Stability:** Supported application authoring and execution contract.<br>
9
- > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.1/packages/runtime/)
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.3/packages/runtime/)
10
11
  >
11
- > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.1` package policy.
12
+ > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.3` package policy.
12
13
  <!-- pulse-package-status:end -->
13
14
 
14
15
  `@pulse-compute/runtime` is the low-level, provider-neutral Pulse application contract.
package/docs/API.md CHANGED
@@ -1,11 +1,11 @@
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.1/reference/handler-authoring/). For provider and target
4
+ JavaScript](https://pulsecompute.io/v1.0.0-beta.3/reference/handler-authoring/). For provider and target
5
5
  differences, use the [compatibility
6
- matrix](https://pulsecompute.io/v1.0.0-beta.1/reference/compatibility-matrix/). For CLI and runtime failures,
6
+ matrix](https://pulsecompute.io/v1.0.0-beta.3/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.1/reference/diagnostics/).
8
+ reference](https://pulsecompute.io/v1.0.0-beta.3/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
 
@@ -30,7 +30,7 @@ process, provider SDK, or global network surface behind it.
30
30
  | [`ctx.emit`](#ctxemit) | HTTP and event handlers | One-way, schema-bound event acceptance effect. |
31
31
  | [`ctx.log`](#ctxlog) | HTTP and event handlers | Synchronous thresholded logging. |
32
32
  | [`ctx.config`, `ctx.secret`](#config-and-secrets) | HTTP and event handlers | Explicit configured binding reads. |
33
- | [`ctx.kv(name)`](#kv) | HTTP and event handlers | Bound `get` and `put` storage effects. |
33
+ | [`ctx.kv(name)`](#kv) | HTTP and event handlers | Bound reads, writes, and conditional KV effects (Node reference). |
34
34
  | [`ctx.json`, `ctx.text`, `ctx.response`](#response-builders) | HTTP handlers and middleware | Synchronous response construction. |
35
35
  | `ctx.event` | Event handlers only | Exact event type and immutable validated payload. |
36
36
 
@@ -49,7 +49,7 @@ Every managed handler is async-shaped:
49
49
  type Handler = (ctx: PulseContext) => Promise<PulseResult | PulseFetchResponse>
50
50
  ```
51
51
 
52
- 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.1/reference/handler-authoring/) defines the static language subset; the [compatibility matrix](https://pulsecompute.io/v1.0.0-beta.1/reference/compatibility-matrix/) owns the tested four-mode claims.
52
+ 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.3/reference/handler-authoring/) defines the static language subset; the [compatibility matrix](https://pulsecompute.io/v1.0.0-beta.3/reference/compatibility-matrix/) owns the tested four-mode claims.
53
53
 
54
54
  ## Static `Router`
55
55
 
@@ -83,7 +83,7 @@ app.mount('/api', api)
83
83
  export default app
84
84
  ```
85
85
 
86
- `Router` is a compile-time marker. Canonical v2 supports `get`, `head`, `post`, exact paths, named parameters, a trailing wildcard, static mounts, global/path-scoped/mounted middleware, route fallthrough, and error middleware. `ctx.param(name)` returns the matched named parameter inside route handlers.
86
+ `Router` is a compile-time marker. Canonical v2 supports `get`, `head`, `post`, `put`, `patch`, `delete`, exact paths, named parameters, a trailing wildcard, static mounts, global/path-scoped/mounted middleware, route fallthrough, and error middleware. `Pulse` inherits these registrations. `ctx.param(name)` returns the matched named parameter inside route handlers.
87
87
 
88
88
  `next()` is a terminal control transfer. `return next()` advances the normal Router cursor and permanently ends the current handler scope. `return next(error)` enters or advances the error lane. There is no onion-style downstream return or post-`next()` resume. Normal and error exhaustion produce compiler-owned 404 and 500 responses respectively.
89
89
 
@@ -137,9 +137,9 @@ eligibility. Fastly targets fail closed because no event ingress/emit adapter is
137
137
  claimed. There is no public event injection command, no event-aware development
138
138
  listener, no HTTP/GRIP translation, and no automatic target fallback.
139
139
 
140
- The [static events guide](https://pulsecompute.io/v1.0.0-beta.1/guides/events/) owns the complete frame,
140
+ The [static events guide](https://pulsecompute.io/v1.0.0-beta.3/guides/events/) owns the complete frame,
141
141
  queue, target-eligibility, diagnostic, and Native-extension contract. The
142
- source-bound [event example](https://pulsecompute.io/v1.0.0-beta.1/examples/11-events/) runs the same mixed project
142
+ source-bound [event example](https://pulsecompute.io/v1.0.0-beta.3/examples/11-events/) runs the same mixed project
143
143
  on Node JavaScript and Node Native.
144
144
 
145
145
  ## `ctx.req`
@@ -400,7 +400,7 @@ const value = await sessions.get('current')
400
400
  if (value !== undefined) await sessions.put('last', value)
401
401
  ```
402
402
 
403
- The Beta supports `get` and `put`. Store names and keys are
403
+ The existing `get` and `put` operations remain available. Store names and keys are
404
404
  provider-neutral logical bindings. Values are bounded, detached, deeply frozen
405
405
  JSON-compatible trees; accessors, symbols, sparse arrays, repeated references,
406
406
  cycles, class instances, nonfinite numbers, and nested `undefined` are rejected.
@@ -408,6 +408,39 @@ cycles, class instances, nonfinite numbers, and nested `undefined` are rejected.
408
408
  and cross-request lifetime remain provider capabilities rather than properties
409
409
  of the common API.
410
410
 
411
+ The Node reference on JavaScript and Native, and the Fastly Native adapter,
412
+ realize these operations through direct await or keyed `ctx.parallel`:
413
+
414
+ | Method | Result |
415
+ | --- | --- |
416
+ | `getVersioned(key)` | `found` with one observed `value` and opaque `generation`, `not-found`, or `failed` with a reason. |
417
+ | `insertIfAbsent(key, value)` | Atomically creates an absent key. |
418
+ | `compareAndSwap(key, generation, value)` | Atomically replaces the value only when its current generation matches. |
419
+
420
+ Conditional writes return `stored`, `conflict`, `not-stored` with a reason, or
421
+ `unknown` with a reason. `unknown` may have committed; Pulse never automatically
422
+ retries or rebases. A write acknowledgement carries no new generation. An absent
423
+ CAS conflicts. A read may be stale, but its value and token describe the same
424
+ observation. Tokens are bounded opaque strings, never JavaScript numbers or
425
+ application revision counters.
426
+
427
+ Namespaces must be literal bindings; keys, generations, and candidates are runtime
428
+ data. Candidates are snapshotted at admission. Keys are exact Unicode scalar
429
+ strings of 1–1,024 UTF-8 bytes without C0/C1 controls; tokens are 1–256 visible
430
+ ASCII bytes. Values retain the 65,536-byte JSON, depth-64 and 10,000-entry bounds.
431
+ The host owns the ten-second operation deadline, shortened by a request deadline.
432
+ Timeouts before dispatch are `not-stored`; unconfirmed writes after dispatch are
433
+ `unknown`. Request cancellation follows the existing managed lifecycle and
434
+ never implies rollback.
435
+
436
+ The Node realization is an explicit in-memory reference instance, not a durable
437
+ storage guarantee. Fastly Native uses lossless 64-bit generation metadata and
438
+ conditional host operations with bounded readiness and body acquisition. Deployed
439
+ cross-location acceptance remains a separate gate. The Fastly JavaScript SDK is
440
+ incomplete capability mapping and does not define or block
441
+ Pulse's contract. Conditional wire values use the strict
442
+ `{"__pulseKv":1,"value":...}` envelope; legacy raw JSON requires explicit migration.
443
+
411
444
  ## Explicit JSON schemas
412
445
 
413
446
  <!-- pulse-doc-source: examples/02-request-schema/.pulse/config.ts -->
@@ -449,12 +482,12 @@ forms lower into canonical package operations; JavaScript targets execute the
449
482
  real package implementation.
450
483
 
451
484
  Older `/pulsewasm` imports are compatibility-only and are isolated in the
452
- [migration guide](https://pulsecompute.io/v1.0.0-beta.1/guides/compatibility-imports/). See the
453
- [GRIP package guide](https://pulsecompute.io/v1.0.0-beta.1/packages/grip/) for the complete current surface.
485
+ [migration guide](https://pulsecompute.io/v1.0.0-beta.3/guides/compatibility-imports/). See the
486
+ [GRIP package guide](https://pulsecompute.io/v1.0.0-beta.3/packages/grip/) for the complete current surface.
454
487
 
455
488
  ## Entities API
456
489
 
457
- `@pulse-compute/entities` is part of the synchronized `1.0.0-beta.1` package
490
+ `@pulse-compute/entities` is part of the synchronized `1.0.0-beta.3` package
458
491
  set. The application surface has two runtime values:
459
492
 
460
493
  ```ts
@@ -516,9 +549,9 @@ Registrations and the terminal binding must use the supported static form. The
516
549
  first-party JSON-RPC adapter accepts bounded JSON-RPC 2.0 request objects and
517
550
  named params, validates declared schemas, uses stable error framing, and
518
551
  acknowledges notifications with HTTP `204`. See the [package
519
- guide](https://pulsecompute.io/v1.0.0-beta.1/packages/entities/), [entity/adapter
520
- model](https://pulsecompute.io/v1.0.0-beta.1/concepts/entities-and-adapters/), and [executable
521
- example](https://pulsecompute.io/v1.0.0-beta.1/examples/10-entities-tools/).
552
+ guide](https://pulsecompute.io/v1.0.0-beta.3/packages/entities/), [entity/adapter
553
+ model](https://pulsecompute.io/v1.0.0-beta.3/concepts/entities-and-adapters/), and [executable
554
+ example](https://pulsecompute.io/v1.0.0-beta.3/examples/10-entities-tools/).
522
555
 
523
556
  ## Project workflow
524
557
 
@@ -535,5 +568,5 @@ selected profile, handler entry, schema declarations, provider bindings, and
535
568
  output directory through `.pulse/config.ts`. `pulse inspect` is optional
536
569
  observability, and `pulse compile` is the advanced provider-neutral Native
537
570
  artifact command; neither is required before `pulse build`. See the [project
538
- lifecycle guide](https://pulsecompute.io/v1.0.0-beta.1/guides/project-lifecycle/) and [CLI
539
- reference](https://pulsecompute.io/v1.0.0-beta.1/reference/cli/).
571
+ lifecycle guide](https://pulsecompute.io/v1.0.0-beta.3/guides/project-lifecycle/) and [CLI
572
+ reference](https://pulsecompute.io/v1.0.0-beta.3/reference/cli/).
@@ -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.1/reference/handler-authoring/); target claims live in the [compatibility matrix](https://pulsecompute.io/v1.0.0-beta.1/reference/compatibility-matrix/).
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/reference/handler-authoring/); target claims live in the [compatibility matrix](https://pulsecompute.io/v1.0.0-beta.3/reference/compatibility-matrix/).
4
4
 
5
5
  <!-- pulse-doc-source: examples/09-router-lowering/src/index.ts -->
6
6
  ```ts
@@ -76,7 +76,7 @@ export default app
76
76
 
77
77
  - `new Router()` with a default-exported root router;
78
78
  - `use(handler)` and `use(path, handler)` middleware;
79
- - `get`, `head`, and `post` registrations;
79
+ - `get`, `head`, `post`, `put`, `patch`, and `delete` registrations;
80
80
  - exact paths, named `:parameters`, and a trailing `*` wildcard;
81
81
  - static acyclic `mount` composition;
82
82
  - named or inline async-shaped route handlers using `(ctx)` or `(ctx, next)`;
@@ -87,6 +87,24 @@ export default app
87
87
 
88
88
  Every Router handler uses the normal canonical context. Fetches, schemas, config, secrets, KV, opaque responses, and trusted package effects lower into the same native execution plan as single-handler authoring.
89
89
 
90
+ `Pulse` inherits the same route registration methods. Each registration matches
91
+ its exact HTTP method; a method mismatch advances the route cursor and normal
92
+ exhaustion remains `404`. There is no automatic `OPTIONS` or `405` response.
93
+ Ingress route methods do not widen the separate outbound `ctx.fetch` contract.
94
+
95
+ The `catalog-router-parity` conformance task exercises the original Catalog
96
+ consumer probe and all 14 of its PUT/PATCH/DELETE operations with their existing
97
+ methods and paths. Its shared cases check mounted parameters, request text,
98
+ middleware state, exact method misses, registration order, terminal fallthrough,
99
+ 404 exhaustion and handled errors on Node/Fastly JavaScript and Native. Native
100
+ lanes execute compiled Wasm; Fastly evidence uses local provider emulation and
101
+ the target ABI mock host, not a deployed service. The fixtures establish routing
102
+ acceptance only; they do not implement Catalog persistence or authorization.
103
+
104
+ ```sh
105
+ node wasm/scripts/run-wasm-tests.cjs --task catalog-router-parity --no-report
106
+ ```
107
+
90
108
  ## `next()` is a terminal transfer
91
109
 
92
110
  `next()` is not an onion-style callback. It is a compiler-visible control transfer:
@@ -1,6 +1,6 @@
1
1
  # Beta scope
2
2
 
3
- Pulse `1.0.0-beta.1` is a Beta of one provider-neutral application contract
3
+ Pulse `1.0.0-beta.3` 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.
@@ -15,7 +15,7 @@ targets.
15
15
  - Async-shaped provider-neutral TypeScript handlers; trusted Pulse awaits lower
16
16
  into explicit effects and continuations without a Promise runtime.
17
17
  - Static Router v2 with terminal middleware, route fallthrough, error
18
- middleware, GET/HEAD/POST routes, exact paths, named parameters, trailing
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
21
  - Request method, URL, path, headers, text, and JSON access.
@@ -106,13 +106,13 @@ JavaScript-selected project into a Native one.
106
106
  ## Compatibility authority
107
107
 
108
108
  The single source-form, provider-binding, artifact, and deployment-boundary
109
- table is [Provider and target compatibility](https://pulsecompute.io/v1.0.0-beta.1/reference/compatibility-matrix/).
109
+ table is [Provider and target compatibility](https://pulsecompute.io/v1.0.0-beta.3/reference/compatibility-matrix/).
110
110
  It uses one public target order—Node JavaScript, Fastly JavaScript, Node Native,
111
111
  and Fastly Native—and links every row to a focused proof or canonical contract.
112
112
 
113
113
  The exact portable language subset and the JavaScript-only Native eligibility
114
114
  boundaries are defined in
115
- [Managed handler TypeScript and JavaScript](https://pulsecompute.io/v1.0.0-beta.1/reference/handler-authoring/).
115
+ [Managed handler TypeScript and JavaScript](https://pulsecompute.io/v1.0.0-beta.3/reference/handler-authoring/).
116
116
 
117
117
  ## Body model
118
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-compute/runtime",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
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.1/packages/runtime/",
44
+ "homepage": "https://pulsecompute.io/v1.0.0-beta.3/packages/runtime/",
45
45
  "bugs": {
46
46
  "url": "https://github.com/pulse-compute/pulse/issues"
47
47
  }
package/src/host.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  Handler,
3
3
  PulseEffect,
4
+ PulseKvGeneration, PulseKvVersionedResult, PulseKvConditionalResult,
4
5
  PulseFetchResponse,
5
6
  PulseParallelEffect,
6
7
  PulseParallelResult,
@@ -14,6 +15,7 @@ export interface PulseJavascriptEffectHostExecution {
14
15
  readonly event?: PulseEventFrame;
15
16
  readonly application?: unknown;
16
17
  readonly signal?: AbortSignal;
18
+ readonly deadlineMonotonicMs?: number;
17
19
  /** Adds execution-owned sensitive text to runtime redaction before provider work continues. */
18
20
  registerRedactionValue(value: string | Uint8Array): void;
19
21
  /** Applies one project-owned schema codec directly to an in-memory semantic value. */
@@ -35,7 +37,26 @@ export interface PulseJavascriptEffectDescriptor {
35
37
  readonly [field: string]: unknown;
36
38
  }
37
39
 
40
+ /** Host-only preparation hook. Resolving/staging must not dispatch; invoking
41
+ * the returned primitive is the conservative storage dispatch boundary. */
42
+ export interface PulseConditionalKvExecution {
43
+ readonly signal?: AbortSignal;
44
+ readonly deadlineMonotonicMs?: number;
45
+ registerRedactionValue?(value: string): void;
46
+ onKvObservation?(value: Readonly<Record<string, unknown>>): void;
47
+ }
48
+ export interface PulseKvClock {
49
+ now(): number;
50
+ setTimeout(callback: () => void, delayMs: number): unknown;
51
+ clearTimeout(handle: unknown): void;
52
+ }
53
+ export type PulseConditionalKvPreparation = (
54
+ effect: Readonly<Record<string, unknown>>,
55
+ execution: PulseConditionalKvExecution
56
+ ) => Promise<(() => unknown) | undefined> | (() => unknown) | undefined;
57
+
38
58
  export interface PulseJavascriptEffectAdapterInput {
59
+ prepareConditionalKv?: PulseConditionalKvPreparation;
39
60
  readonly id?: string;
40
61
  dispatch(
41
62
  effect: PulseJavascriptEffectDescriptor,
@@ -46,6 +67,7 @@ export interface PulseJavascriptEffectAdapterInput {
46
67
 
47
68
  export interface PulseJavascriptEffectAdapter {
48
69
  readonly version: 'pulse.javascript-effect-adapter.v1';
70
+ readonly prepareConditionalKv?: PulseConditionalKvPreparation;
49
71
  readonly id: string;
50
72
  dispatch(
51
73
  effect: PulseJavascriptEffectDescriptor,
@@ -80,12 +102,16 @@ export interface PulseEventRecordingAdapter extends PulseJavascriptEffectAdapter
80
102
  }
81
103
 
82
104
  export interface PulseRuntimeHostKvNamespace<T = unknown> {
105
+ getVersioned?(key: string): Promise<PulseKvVersionedResult<T>> | PulseKvVersionedResult<T>;
106
+ insertIfAbsent?(key: string, value: T): Promise<PulseKvConditionalResult> | PulseKvConditionalResult;
107
+ compareAndSwap?(key: string, generation: PulseKvGeneration, value: T): Promise<PulseKvConditionalResult> | PulseKvConditionalResult;
83
108
  get(key: string): Promise<T | undefined> | T | undefined;
84
109
  put(key: string, value: T): Promise<boolean> | boolean;
85
110
  }
86
111
 
87
112
  /** Compatibility injection shape retained while providers move to one effect adapter. */
88
113
  export interface PulseRuntimeHostCapabilities {
114
+ prepareConditionalKv?: PulseConditionalKvPreparation;
89
115
  fetch?(
90
116
  url: string,
91
117
  init?: unknown,
@@ -115,7 +141,7 @@ export interface PulseRuntimeHostCapabilities {
115
141
 
116
142
  export interface PulseJavascriptEffectObservation {
117
143
  readonly version: 'pulse.javascript-effect-observation.v1';
118
- readonly type: 'effect-dispatched' | 'effect-settled' | 'parallel-dispatched' | 'parallel-settled';
144
+ readonly type: 'effect-dispatched' | 'effect-settled' | 'parallel-dispatched' | 'parallel-settled' | 'kv-lifecycle';
119
145
  readonly [field: string]: unknown;
120
146
  }
121
147
 
@@ -129,6 +155,7 @@ export interface PulseJavascriptEffectSummary {
129
155
  readonly version: 'pulse.javascript-effect-observation.v1';
130
156
  readonly adapter: {
131
157
  readonly version: 'pulse.javascript-effect-adapter.v1';
158
+ readonly prepareConditionalKv?: PulseConditionalKvPreparation;
132
159
  readonly id: string;
133
160
  };
134
161
  readonly effectCount: number;
@@ -173,6 +200,10 @@ export interface PulseJavascriptEffectExecution {
173
200
  }
174
201
 
175
202
  export interface PulseRuntimeExecutionOptions {
203
+ /** Host-only monotonic clock/scheduler injection for deterministic lifecycle evidence. */
204
+ readonly kvClock?: PulseKvClock;
205
+ /** Host monotonic deadline; conditional KV uses the earlier of this and 10 seconds. */
206
+ readonly deadlineMonotonicMs?: number;
176
207
  /** Compatibility bridge. New providers should inject one shared effectAdapter instead. */
177
208
  readonly capabilities?: PulseRuntimeHostCapabilities;
178
209
  readonly effectAdapter?: PulseJavascriptEffectAdapter | PulseJavascriptEffectAdapterInput;
@@ -374,3 +405,16 @@ export declare const EVENT_EMIT_CODES: Readonly<{
374
405
  QUEUE_FULL: 'PULSE_RUNTIME_EVENT_EMIT_QUEUE_FULL';
375
406
  ADAPTER_INVALID: 'PULSE_RUNTIME_EVENT_ADAPTER_INVALID';
376
407
  }>;
408
+
409
+ /** Shared host-only conditional KV boundary; no provider SDK tokens enter as numbers. */
410
+ export declare const KV_CONDITIONAL_KINDS: readonly string[];
411
+ export declare const KV_CONDITIONAL_LIMITS: Readonly<Record<string, number>>;
412
+ export declare function isConditionalKv(kind: string): boolean;
413
+ export declare function normalizeKvGeneration(value: unknown): PulseKvGeneration;
414
+ export declare function normalizeConditionalKvKey(value: unknown, options?: PulseBindingValueLimits): string;
415
+ export declare function encodeConditionalKvValue(value: unknown, options?: PulseBindingValueLimits): Uint8Array;
416
+ export declare function decodeConditionalKvValue(bytes: Uint8Array, options?: PulseBindingValueLimits): unknown;
417
+ export declare function admitConditionalKv(input: Readonly<Record<string, unknown>>, options?: PulseRuntimeExecutionOptions): Readonly<Record<string, unknown>>;
418
+ export declare function normalizeConditionalKvResult(effect: Readonly<{ kind: string }>, value: unknown, options?: PulseBindingValueLimits): PulseKvVersionedResult<unknown> | PulseKvConditionalResult;
419
+ export declare function executeConditionalKv(effect: Readonly<Record<string, unknown>>, prepare: PulseConditionalKvPreparation, execution?: PulseConditionalKvExecution, options?: PulseRuntimeExecutionOptions): Promise<PulseKvVersionedResult<unknown> | PulseKvConditionalResult>;
420
+ export declare function registerKvRedactions(effect: Readonly<Record<string, unknown>>, register?: (value: string) => void): void;
package/src/host.js CHANGED
@@ -78,6 +78,7 @@ async function executeApplication(application, request, options = {}) {
78
78
  }
79
79
 
80
80
  module.exports = Object.freeze({
81
+ ...require('./internal/conditional-kv.js'),
81
82
  RUNTIME_HOST_API_VERSION,
82
83
  JAVASCRIPT_EFFECT_PROTOCOL_VERSION,
83
84
  JAVASCRIPT_EFFECT_ADAPTER_VERSION,
package/src/index.d.ts CHANGED
@@ -101,7 +101,30 @@ export interface PulseLogger {
101
101
  debug(message: string): void;
102
102
  }
103
103
 
104
+ // Opaque, bounded string. It is neither an application version nor a number.
105
+ export type PulseKvGeneration = string;
106
+ export type PulseKvReadFailureReason =
107
+ | 'invalid-key' | 'configuration' | 'not-authorized' | 'throttled'
108
+ | 'unavailable' | 'transport' | 'timeout' | 'protocol' | 'too-large' | 'invalid-value';
109
+ export type PulseKvVersionedResult<T> =
110
+ | { readonly status: 'found'; readonly value: T; readonly generation: PulseKvGeneration }
111
+ | { readonly status: 'not-found' }
112
+ | { readonly status: 'failed'; readonly reason: PulseKvReadFailureReason };
113
+
114
+ export type PulseKvNotStoredReason =
115
+ | 'invalid-key' | 'invalid-value' | 'invalid-generation' | 'too-large'
116
+ | 'configuration' | 'not-authorized' | 'throttled' | 'rejected'
117
+ | 'unavailable' | 'transport' | 'timeout' | 'protocol';
118
+ export type PulseKvConditionalResult =
119
+ | { readonly status: 'stored' }
120
+ | { readonly status: 'conflict' }
121
+ | { readonly status: 'not-stored'; readonly reason: PulseKvNotStoredReason }
122
+ | { readonly status: 'unknown'; readonly reason: 'transport' | 'timeout' | 'unavailable' | 'protocol' };
123
+
104
124
  export interface PulseKvNamespace<T = unknown> {
125
+ getVersioned(key: string): PulseParallelEffect<PulseKvVersionedResult<T>>;
126
+ insertIfAbsent(key: string, value: T): PulseParallelEffect<PulseKvConditionalResult>;
127
+ compareAndSwap(key: string, generation: PulseKvGeneration, value: T): PulseParallelEffect<PulseKvConditionalResult>;
105
128
  get(key: string): PulseParallelEffect<T | undefined>;
106
129
  put(key: string, value: T): PulseParallelEffect<boolean>;
107
130
  }
@@ -172,7 +195,7 @@ export type PulseErrorHandler = RouterErrorHandler;
172
195
 
173
196
  /**
174
197
  * Static authoring marker compiled by Pulse. Router instances are not runtime dispatchers.
175
- * Canonical v2 supports terminal middleware/error transfer, get/head/post routes,
198
+ * Canonical v2 supports terminal middleware/error transfer, get/head/post/put/patch/delete routes,
176
199
  * fallthrough, and acyclic static mounts.
177
200
  */
178
201
  export declare class Router {
@@ -181,6 +204,9 @@ export declare class Router {
181
204
  get(path: string, handler: RouteHandler): this;
182
205
  head(path: string, handler: RouteHandler): this;
183
206
  post(path: string, handler: RouteHandler): this;
207
+ put(path: string, handler: RouteHandler): this;
208
+ patch(path: string, handler: RouteHandler): this;
209
+ delete(path: string, handler: RouteHandler): this;
184
210
  mount(path: string, router: Router): this;
185
211
  error(handler: RouterErrorHandler): this;
186
212
  }
@@ -171,7 +171,7 @@ function clonePortableKvValue(value, state, path, depth) {
171
171
  }
172
172
  const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
173
173
  if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
174
- throw kvValueError('Pulse KV arrays may not contain accessors.', `${path}[${index}]`, value[index]);
174
+ throw kvValueError('Pulse KV arrays may not contain accessors.', `${path}[${index}]`, undefined);
175
175
  }
176
176
  definePortableProperty(output, String(index), clonePortableKvValue(descriptor.value, state, `${path}[${index}]`, depth + 1));
177
177
  }
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ // Shared host contract. Provider preparation must not send a storage operation;
4
+ // the returned primitive is the conservative dispatch boundary (including throws).
5
+ const { cloneKvValue, normalizeKvNamespace } = require('./bindings.js');
6
+ const { PulseRuntimeContractError } = require('./errors.js');
7
+ const KV_CONDITIONAL_KINDS = Object.freeze(['kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap']);
8
+ const KV_CONDITIONAL_LIMITS = Object.freeze({ namespaceBytes: 256, keyBytes: 1024, generationBytes: 256,
9
+ valueBytes: 65536, wireBytes: 65560, depth: 64, entries: 10000, envelopeBytes: 262144, timeoutMs: 10000 });
10
+ const readReasons = new Set(['invalid-key', 'configuration', 'not-authorized', 'throttled', 'unavailable', 'transport', 'timeout', 'protocol', 'too-large', 'invalid-value']);
11
+ const writeReasons = new Set([...readReasons, 'invalid-generation', 'rejected']);
12
+ const unknownReasons = new Set(['transport', 'timeout', 'unavailable', 'protocol']);
13
+ const encoder = new TextEncoder();
14
+ const admissions = new WeakSet();
15
+ // Descriptor copies retain the original admission/deadline. Symbols never enter
16
+ // the serialized effect envelope; only admissions minted by this owner are trusted.
17
+ const admissionSymbol = Symbol('pulse.conditional-kv-admission');
18
+ const isConditionalKv = (kind) => KV_CONDITIONAL_KINDS.includes(kind);
19
+ const isRead = (effect) => effect.kind === 'kv.getVersioned';
20
+ const frozen = (value) => Object.freeze(value);
21
+ function fail(reason) { const error = new Error('Pulse conditional KV boundary rejected data.'); error.kvReason = reason; throw error; }
22
+ function byteLength(value) { return encoder.encode(value).byteLength; }
23
+ function kvClock(options = {}) { return options.kvClock || { now: () => performance.now(), setTimeout, clearTimeout }; }
24
+ function boundedLimits(options = {}) {
25
+ const limit = (name, max) => options[name] === undefined ? max : Math.min(max, options[name]);
26
+ return { maxKvValueBytes: limit('maxKvValueBytes', 65536), maxKvValueDepth: limit('maxKvValueDepth', 64), maxKvValueEntries: limit('maxKvValueEntries', 10000) };
27
+ }
28
+ function normalizeKvGeneration(value) {
29
+ if (typeof value !== 'string' || !/^[\x21-\x7e]{1,256}$/.test(value)) fail('invalid-generation');
30
+ return value;
31
+ }
32
+ function normalizeConditionalKvKey(value, options = {}) {
33
+ if (typeof value !== 'string' || value.length === 0 || value.length > 1024 || byteLength(value) > Math.min(1024, options.maxKvKeyBytes ?? 1024)
34
+ || /[\u0000-\u001f\u007f-\u009f]/u.test(value) || /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value)) fail('invalid-key');
35
+ return value;
36
+ }
37
+ function validationReason(error) {
38
+ if (error && error.kvReason) return error.kvReason;
39
+ return /TOO_LARGE|DEPTH_EXCEEDED|ENTRIES_EXCEEDED/.test(error && error.code || '') ? 'too-large' : 'invalid-value';
40
+ }
41
+ function encodeConditionalKvValue(value, options = {}) {
42
+ let detached;
43
+ try { detached = cloneKvValue(value, boundedLimits(options)); } catch (error) { fail(validationReason(error)); }
44
+ const wire = encoder.encode('{"__pulseKv":1,"value":' + JSON.stringify(detached) + '}');
45
+ if (wire.byteLength > KV_CONDITIONAL_LIMITS.wireBytes) fail('too-large');
46
+ return wire;
47
+ }
48
+ function decodeConditionalKvValue(bytes, options = {}) {
49
+ if (!(bytes instanceof Uint8Array)) fail('protocol');
50
+ if (bytes.byteLength > KV_CONDITIONAL_LIMITS.wireBytes) fail('too-large');
51
+ let text, parsed;
52
+ try {
53
+ text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
54
+ parsed = JSON.parse(text);
55
+ // JSON.parse validates syntax; this bounded scan rejects last-wins duplicate
56
+ // members, including escaped aliases, before exposing any parsed value.
57
+ const tokens = text.match(/"(?:[^"\\]|\\.)*"|[{}\[\],:]|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g);
58
+ let cursor = 0, entries = 0;
59
+ function visit(depth) {
60
+ if (depth > 65 || ++entries > 10002) fail('too-large');
61
+ const first = tokens[cursor++];
62
+ if (first !== '{' && first !== '[') return;
63
+ const end = first === '{' ? '}' : ']', names = new Set();
64
+ if (tokens[cursor] === end) { cursor++; return; }
65
+ while (true) {
66
+ if (first === '{') {
67
+ const name = JSON.parse(tokens[cursor++]);
68
+ if (names.has(name)) fail('protocol'); names.add(name);
69
+ if (tokens[cursor++] !== ':') fail('protocol');
70
+ }
71
+ visit(depth + 1);
72
+ const separator = tokens[cursor++];
73
+ if (separator === end) return;
74
+ if (separator !== ',') fail('protocol');
75
+ }
76
+ }
77
+ visit(0);
78
+ if (cursor !== tokens.length || !parsed || Array.isArray(parsed) || parsed.__pulseKv !== 1
79
+ || Object.keys(parsed).sort().join(',') !== '__pulseKv,value') fail('protocol');
80
+ } catch (error) { fail(error.kvReason || 'protocol'); }
81
+ try { return cloneKvValue(parsed.value, boundedLimits(options)); } catch (error) { fail(validationReason(error)); }
82
+ }
83
+ function kvFailure(effect, reason, dispatched = false) {
84
+ return frozen(isRead(effect) ? { status: 'failed', reason: readReasons.has(reason) ? reason : 'protocol' }
85
+ : { status: dispatched ? 'unknown' : 'not-stored', reason: (dispatched ? unknownReasons : writeReasons).has(reason) ? reason : 'protocol' });
86
+ }
87
+ function admitConditionalKv(input, options = {}) {
88
+ if (admissions.has(input)) return input;
89
+ if (input && admissions.has(input[admissionSymbol])) return input[admissionSymbol];
90
+ const clock = kvClock(options);
91
+ const deadline = Math.min(clock.now() + KV_CONDITIONAL_LIMITS.timeoutMs, options.deadlineMonotonicMs ?? Infinity);
92
+ const result = { kind: input.kind, deadline };
93
+ try {
94
+ try { result.namespace = normalizeKvNamespace(input.namespace ?? input.store, { maxKvNamespaceBytes: Math.min(256, options.maxKvNamespaceBytes ?? 256) }); }
95
+ catch (_) { fail('configuration'); }
96
+ result.key = normalizeConditionalKvKey(input.key, options);
97
+ if (input.kind === 'kv.compareAndSwap') result.generation = normalizeKvGeneration(input.generation);
98
+ if (!isRead(input)) result.value = decodeConditionalKvValue(encodeConditionalKvValue(input.value, options), options);
99
+ if (byteLength(JSON.stringify(result)) > KV_CONDITIONAL_LIMITS.envelopeBytes) fail('too-large');
100
+ } catch (error) { result.validationResult = kvFailure(result, validationReason(error)); }
101
+ result[admissionSymbol] = result;
102
+ const admitted = frozen(result); admissions.add(admitted); return admitted;
103
+ }
104
+ function exactRecord(value, fields) {
105
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('protocol');
106
+ const prototype = Object.getPrototypeOf(value);
107
+ if (prototype !== Object.prototype && prototype !== null) fail('protocol');
108
+ const keys = Reflect.ownKeys(value);
109
+ if (keys.length !== fields.length || !fields.every((key) => keys.includes(key))) fail('protocol');
110
+ for (const key of keys) {
111
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
112
+ if (!descriptor.enumerable || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) fail('protocol');
113
+ }
114
+ }
115
+ function normalizeConditionalKvResult(effect, value, options = {}) {
116
+ try {
117
+ // Read status without invoking an accessor from a malformed provider result.
118
+ const status = value && Object.getOwnPropertyDescriptor(value, 'status');
119
+ const tag = status && status.value;
120
+ if (isRead(effect)) {
121
+ if (tag === 'found') {
122
+ exactRecord(value, ['status', 'value', 'generation']);
123
+ const generation = normalizeKvGeneration(value.generation);
124
+ const detached = decodeConditionalKvValue(encodeConditionalKvValue(value.value, options), options);
125
+ return frozen({ status: 'found', value: detached, generation });
126
+ }
127
+ if (tag === 'not-found') { exactRecord(value, ['status']); return frozen({ status: tag }); }
128
+ exactRecord(value, ['status', 'reason']);
129
+ if (tag !== 'failed' || !readReasons.has(value.reason)) fail('protocol');
130
+ } else {
131
+ if (tag === 'stored' || tag === 'conflict') { exactRecord(value, ['status']); return frozen({ status: tag }); }
132
+ exactRecord(value, ['status', 'reason']);
133
+ if (!((tag === 'not-stored' && writeReasons.has(value.reason)) || (tag === 'unknown' && unknownReasons.has(value.reason)))) fail('protocol');
134
+ }
135
+ return frozen({ status: tag, reason: value.reason });
136
+ } catch (error) { return kvFailure(effect, validationReason(error) === 'invalid-generation' ? 'protocol' : validationReason(error), !isRead(effect)); }
137
+ }
138
+ function registerKvRedactions(effect, register) {
139
+ if (typeof register !== 'function') return;
140
+ const visit = (value) => {
141
+ if (typeof value === 'string' && value.length) register(value);
142
+ else if (value && typeof value === 'object') for (const key of Object.keys(value)) { register(key); visit(value[key]); }
143
+ };
144
+ visit(effect.key); visit(effect.generation); visit(effect.value);
145
+ }
146
+ function cancelled() { return new PulseRuntimeContractError('PULSE_RUNTIME_EFFECT_ABORTED', 'Pulse conditional KV execution was cancelled.'); }
147
+ async function executeConditionalKv(effect, prepare, execution = {}, options = {}) {
148
+ const admitted = admissions.has(effect) ? effect : admitConditionalKv(effect, options);
149
+ const clock = kvClock(options), controller = new AbortController();
150
+ let dispatched = false, settled = false, timer, rejectCancellation, resolveTimeout;
151
+ registerKvRedactions(admitted, execution.registerRedactionValue);
152
+ const observe = (outcome) => {
153
+ if (typeof execution.onKvObservation === 'function') execution.onKvObservation(frozen({
154
+ type: 'kv-lifecycle', kind: admitted.kind, dispatched, mayHaveCommitted: !isRead(admitted) && dispatched && (outcome === 'cancelled' || outcome === 'unknown'), outcome
155
+ }));
156
+ };
157
+ const cancellation = new Promise((_, reject) => { rejectCancellation = reject; });
158
+ const timeout = new Promise((resolve) => { resolveTimeout = resolve; });
159
+ const abort = () => { if (!settled) { controller.abort(); rejectCancellation(cancelled()); } };
160
+ const expire = () => { if (!settled) { controller.abort(); resolveTimeout(kvFailure(admitted, 'timeout', dispatched)); } };
161
+ const signal = execution.signal;
162
+ if (signal) signal.addEventListener('abort', abort, { once: true });
163
+ try {
164
+ if (signal && signal.aborted) throw cancelled();
165
+ if (admitted.validationResult) return admitted.validationResult;
166
+ if (clock.now() >= admitted.deadline) return kvFailure(admitted, 'timeout');
167
+ timer = clock.setTimeout(expire, admitted.deadline - clock.now());
168
+ const operationExecution = frozen({ ...execution, signal: controller.signal, deadlineMonotonicMs: admitted.deadline });
169
+ const work = Promise.resolve().then(async () => {
170
+ if (controller.signal.aborted) return kvFailure(admitted, 'timeout');
171
+ try {
172
+ const primitive = await prepare(admitted, operationExecution);
173
+ if (controller.signal.aborted || clock.now() >= admitted.deadline) {
174
+ controller.abort();
175
+ return kvFailure(admitted, 'timeout');
176
+ }
177
+ if (typeof primitive !== 'function') return kvFailure(admitted, 'configuration');
178
+ dispatched = true;
179
+ const value = await primitive();
180
+ if (clock.now() >= admitted.deadline) {
181
+ controller.abort();
182
+ return kvFailure(admitted, 'timeout', true);
183
+ }
184
+ const normalized = normalizeConditionalKvResult(admitted, value, options);
185
+ if (normalized.status === 'found') registerKvRedactions(normalized, execution.registerRedactionValue);
186
+ return normalized;
187
+ } catch (error) {
188
+ return kvFailure(admitted, dispatched ? 'transport' : error && writeReasons.has(error.kvReason) ? error.kvReason : 'unavailable', dispatched);
189
+ }
190
+ });
191
+ const result = await Promise.race([work, timeout, cancellation]);
192
+ if (signal && signal.aborted) throw cancelled();
193
+ observe(result.status);
194
+ return result;
195
+ } catch (error) { if (signal && signal.aborted) observe('cancelled'); throw error; }
196
+ finally { settled = true; if (timer !== undefined) clock.clearTimeout(timer); if (signal) signal.removeEventListener('abort', abort); }
197
+ }
198
+ module.exports = Object.freeze({ KV_CONDITIONAL_KINDS, KV_CONDITIONAL_LIMITS, isConditionalKv, normalizeKvGeneration, normalizeConditionalKvKey,
199
+ admitConditionalKv, encodeConditionalKvValue, decodeConditionalKvValue, normalizeConditionalKvResult, executeConditionalKv, registerKvRedactions });
@@ -207,6 +207,10 @@ function createContext(frame) {
207
207
  kv(name) {
208
208
  const namespace = normalizeKvNamespace(name, options);
209
209
  return Object.freeze({
210
+ ...Object.fromEntries(['getVersioned', 'insertIfAbsent', 'compareAndSwap'].map((operation) => [operation, (key, generationOrValue, value) => effects.dispatch({
211
+ kind: `kv.${operation}`, providerKind: 'kv', operation, capability: `kv.${operation}`, namespace, key,
212
+ ...(operation === 'compareAndSwap' ? { generation: generationOrValue, value } : operation === 'insertIfAbsent' ? { value: generationOrValue } : {})
213
+ })])),
210
214
  get(key) {
211
215
  const normalizedKey = normalizeKvKey(key, options);
212
216
  return effects.dispatch({
@@ -9,6 +9,7 @@ const {
9
9
  normalizeKvNamespace,
10
10
  normalizeKvPutResult
11
11
  } = require('./bindings.js');
12
+ const conditionalKv = require('./conditional-kv.js');
12
13
  const { fetchRequestInitForHost } = require('./fetch.js');
13
14
  const { createRedactionState } = require('./redaction.js');
14
15
  const { validateSchemaValue } = require('./schema.js');
@@ -102,7 +103,6 @@ function createEffectSignal(lifecycleSignal, descriptor) {
102
103
 
103
104
  function raceWithSignal(promise, signal) {
104
105
  if (!signal) return Promise.resolve(promise);
105
- if (signal.aborted) return Promise.reject(abortedEffectError(signal.reason));
106
106
  return new Promise((resolve, reject) => {
107
107
  let settled = false;
108
108
  const finish = (callback, value) => {
@@ -117,6 +117,8 @@ function raceWithSignal(promise, signal) {
117
117
  (value) => finish(resolve, value),
118
118
  (error) => finish(reject, error)
119
119
  );
120
+ // Observe the owned promise even when cancellation preceded the race.
121
+ if (signal.aborted) onAbort();
120
122
  });
121
123
  }
122
124
 
@@ -167,6 +169,7 @@ function publicEffectDescriptor(effect, redaction) {
167
169
  if (effect.kind === 'config.get' || effect.kind === 'secret.get') {
168
170
  return Object.freeze({ ...base, name: redactString(effect.name) });
169
171
  }
172
+ if (conditionalKv.isConditionalKv(effect.kind)) return Object.freeze({ ...base, namespace: redactString(effect.namespace), key: '<redacted>', generation: '<redacted>', value: '<redacted>' });
170
173
  if (effect.kind === 'kv.get' || effect.kind === 'kv.put') {
171
174
  return Object.freeze({
172
175
  ...base,
@@ -208,6 +211,7 @@ function createJavascriptEffectAdapter(input) {
208
211
  version: JAVASCRIPT_EFFECT_ADAPTER_VERSION,
209
212
  id: String(input.id || 'pulse.javascript-effect-adapter.custom'),
210
213
  dispatch,
214
+ prepareConditionalKv: typeof input.prepareConditionalKv === 'function' ? input.prepareConditionalKv : undefined,
211
215
  dispose
212
216
  });
213
217
  }
@@ -225,6 +229,16 @@ function createCapabilityEffectAdapter(capabilities = {}) {
225
229
  const namespaces = new Map();
226
230
  return createJavascriptEffectAdapter({
227
231
  id: 'pulse.javascript-effect-adapter.capabilities',
232
+ async prepareConditionalKv(effect, execution) {
233
+ if (typeof value.prepareConditionalKv === 'function') return value.prepareConditionalKv(effect, execution);
234
+ if (typeof value.kv !== 'function') return undefined;
235
+ if (!namespaces.has(effect.namespace)) namespaces.set(effect.namespace, await value.kv(effect.namespace, execution));
236
+ const namespace = namespaces.get(effect.namespace), method = effect.kind.slice(3);
237
+ if (!namespace || typeof namespace[method] !== 'function') return undefined;
238
+ return () => method === 'getVersioned' ? namespace[method](effect.key, execution)
239
+ : method === 'insertIfAbsent' ? namespace[method](effect.key, effect.value, execution)
240
+ : namespace[method](effect.key, effect.generation, effect.value, execution);
241
+ },
228
242
  async dispatch(effect, execution) {
229
243
  if (effect.kind === 'fetch') {
230
244
  if (typeof value.fetch !== 'function') return unavailableCapability(effect);
@@ -260,6 +274,7 @@ function createCapabilityEffectAdapter(capabilities = {}) {
260
274
 
261
275
  function bindingLimits(options = {}) {
262
276
  return Object.freeze({
277
+ kvClock: options.kvClock, deadlineMonotonicMs: options.deadlineMonotonicMs,
263
278
  maxBindingNameBytes: options.maxBindingNameBytes,
264
279
  maxBindingValueBytes: options.maxBindingValueBytes,
265
280
  maxKvNamespaceBytes: options.maxKvNamespaceBytes,
@@ -272,6 +287,7 @@ function bindingLimits(options = {}) {
272
287
 
273
288
  function normalizeEffectDescriptor(input, limits) {
274
289
  const kind = String(input.kind || 'effect');
290
+ if (conditionalKv.isConditionalKv(kind)) return { ...conditionalKv.admitConditionalKv(input, limits), id: input.id, parallelEligible: input.parallelEligible };
275
291
  if (kind === 'config.get' || kind === 'secret.get') {
276
292
  const bindingKind = kind === 'secret.get' ? 'secret' : 'config';
277
293
  return { ...input, name: normalizeBindingName(bindingKind, input.name, limits) };
@@ -574,6 +590,7 @@ function createJavascriptEffectExecution(options = {}) {
574
590
  }
575
591
  dispatchedIds.add(descriptor.id);
576
592
  totalEffects += 1;
593
+ if (conditionalKv.isConditionalKv(kind)) conditionalKv.registerKvRedactions(descriptor, (value) => redaction.add(value));
577
594
  const publicDescriptor = publicEffectDescriptor(descriptor, redaction);
578
595
  observe({ type: 'effect-dispatched', effect: publicDescriptor });
579
596
 
@@ -581,6 +598,9 @@ function createJavascriptEffectExecution(options = {}) {
581
598
  const operationExecution = Object.freeze({ ...externalExecution, signal: operationSignal.signal });
582
599
  const raw = raceWithSignal(Promise.resolve().then(() => {
583
600
  if (operationSignal.signal.aborted) throw abortedEffectError(operationSignal.signal.reason);
601
+ if (conditionalKv.isConditionalKv(kind)) return conditionalKv.executeConditionalKv(descriptor,
602
+ adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatch(descriptor, kvExecution)),
603
+ { ...operationExecution, onKvObservation: observe }, limits);
584
604
  return adapter.dispatch(descriptor, operationExecution);
585
605
  }), operationSignal.signal).then(
586
606
  (value) => {
@@ -317,6 +317,7 @@ function effectExecutionOptions(options, application, frame) {
317
317
  executionKind: 'event',
318
318
  signal: options.signal,
319
319
  maxEffects: options.maxEffects,
320
+ kvClock: options.kvClock, deadlineMonotonicMs: options.deadlineMonotonicMs,
320
321
  maxBindingNameBytes: options.maxBindingNameBytes,
321
322
  maxBindingValueBytes: options.maxBindingValueBytes,
322
323
  maxKvNamespaceBytes: options.maxKvNamespaceBytes,
@@ -16,6 +16,13 @@ const DEFAULT_MAX_PAYLOAD_ENTRIES = 4096;
16
16
  const PACKAGE_CONTEXTS = new WeakMap();
17
17
 
18
18
  const TRUSTED_PACKAGE_EFFECT_CATALOG = Object.freeze({
19
+ '@pulse-compute/s3': Object.freeze({
20
+ contractId: 'pulse.s3', providerKind: 's3', operations: Object.freeze({
21
+ head: Object.freeze({ kind: 's3.head', capability: 's3.head', result: 's3-head-result' }),
22
+ getText: Object.freeze({ kind: 's3.getText', capability: 's3.getText', result: 's3-get-text-result' }),
23
+ putText: Object.freeze({ kind: 's3.putText', capability: 's3.putText', result: 's3-put-text-result' })
24
+ })
25
+ }),
19
26
  '@pulse-compute/assets': Object.freeze({
20
27
  contractId: 'pulse.assets',
21
28
  providerKind: 'assets',
@@ -84,6 +84,33 @@ class Router {
84
84
  return this;
85
85
  }
86
86
 
87
+ put(path, handler) {
88
+ ROUTER_STATE.get(this).entries.push(entry('route', {
89
+ method: 'PUT',
90
+ path: compileRoutePath(path, { allowWildcard: true }),
91
+ handler: requireHandler(handler, 'put(path, handler)')
92
+ }));
93
+ return this;
94
+ }
95
+
96
+ patch(path, handler) {
97
+ ROUTER_STATE.get(this).entries.push(entry('route', {
98
+ method: 'PATCH',
99
+ path: compileRoutePath(path, { allowWildcard: true }),
100
+ handler: requireHandler(handler, 'patch(path, handler)')
101
+ }));
102
+ return this;
103
+ }
104
+
105
+ delete(path, handler) {
106
+ ROUTER_STATE.get(this).entries.push(entry('route', {
107
+ method: 'DELETE',
108
+ path: compileRoutePath(path, { allowWildcard: true }),
109
+ handler: requireHandler(handler, 'delete(path, handler)')
110
+ }));
111
+ return this;
112
+ }
113
+
87
114
  mount(path, router) {
88
115
  ROUTER_STATE.get(this).entries.push(entry('mount', {
89
116
  path: compileRoutePath(path, { scoped: true, allowWildcard: true }),
@@ -391,6 +418,7 @@ async function executeRouter(router, request, options = {}) {
391
418
  executionKind: 'request',
392
419
  signal: executionSignal,
393
420
  maxEffects: options.maxEffects,
421
+ kvClock: options.kvClock, deadlineMonotonicMs: options.deadlineMonotonicMs,
394
422
  maxBindingNameBytes: options.maxBindingNameBytes,
395
423
  maxBindingValueBytes: options.maxBindingValueBytes,
396
424
  maxKvNamespaceBytes: options.maxKvNamespaceBytes,