@pulse-compute/pulse 0.0.0 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -1
- package/package.json +36 -5
- package/src/index.d.ts +164 -0
- package/src/index.js +8 -0
- package/src/internal/application.js +75 -0
- package/src/internal/event-registration.js +140 -0
- package/src/internal/index.js +3 -0
- package/src/schema.d.ts +51 -0
- package/src/schema.js +45 -0
package/README.md
CHANGED
|
@@ -1,3 +1,193 @@
|
|
|
1
1
|
# @pulse-compute/pulse
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<!-- pulse-package-status:start -->
|
|
4
|
+
> **Support tier:** Canonical application surface<br>
|
|
5
|
+
> **Audience:** Pulse application authors using the conventional project root, configuration factory, and schema declarations.<br>
|
|
6
|
+
> **Install directly:** Yes. Install it in every conventional Pulse project.<br>
|
|
7
|
+
> **Supported entry points:** `@pulse-compute/pulse`, `@pulse-compute/pulse/schema`<br>
|
|
8
|
+
> **Stability:** Supported conventional application, project-configuration, and schema-authoring contract.<br>
|
|
9
|
+
> **npm:** [`@pulse-compute/pulse`](https://www.npmjs.com/package/@pulse-compute/pulse)<br>
|
|
10
|
+
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.2/packages/pulse/)
|
|
11
|
+
>
|
|
12
|
+
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.2` package policy.
|
|
13
|
+
<!-- pulse-package-status:end -->
|
|
14
|
+
|
|
15
|
+
This package owns the project-aware authoring layer above the low-level
|
|
16
|
+
`@pulse-compute/runtime` contract. It currently exports:
|
|
17
|
+
|
|
18
|
+
- `defineConfig((scope) => ({ ... }))`;
|
|
19
|
+
- symbolic `scope.config()` and `scope.secret()` references;
|
|
20
|
+
- the conventional `Pulse` application root;
|
|
21
|
+
- the project-level TypeScript contract used by `.pulse/config.ts`.
|
|
22
|
+
|
|
23
|
+
`Pulse` extends the TypeScript `Router` surface for authoring convenience, but it
|
|
24
|
+
does not create a second routing system. Compiler analysis normalizes a direct
|
|
25
|
+
`new Pulse(...)` root to the existing Router IR and attaches project/profile
|
|
26
|
+
metadata beside that IR. Router registration order, matching, terminal `next()`,
|
|
27
|
+
error lanes, 404/500 ownership, effects, continuations, and provider realization
|
|
28
|
+
remain unchanged.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Generated project contract
|
|
32
|
+
|
|
33
|
+
`pulse init` generates the conventional application package directly:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
.pulse/config.ts
|
|
37
|
+
.pulse/.gitignore
|
|
38
|
+
src/index.ts
|
|
39
|
+
tests/pulse.harness.ts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The generated application uses `new Pulse({ auto: true })`, async managed
|
|
43
|
+
handlers, the default `local` native profile, and strict schema policy. The
|
|
44
|
+
generated project depends on this package explicitly; it does not rely on an
|
|
45
|
+
undocumented transitive application root.
|
|
46
|
+
|
|
47
|
+
## Application root
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { Pulse } from '@pulse-compute/pulse'
|
|
51
|
+
|
|
52
|
+
const app = new Pulse({ auto: true })
|
|
53
|
+
|
|
54
|
+
app.get('/health', async (ctx) => {
|
|
55
|
+
return ctx.json({ ok: true })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export default app
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The compiler accepts only direct, statically legible construction forms:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
new Pulse({ auto: true })
|
|
65
|
+
new Pulse(configFactory)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Aliases, subclasses, factories returning `Pulse`, mounted `Pulse` instances, and
|
|
69
|
+
re-exported application roots remain outside the single-entry grammar.
|
|
70
|
+
Mounted and child applications continue to use `Router`.
|
|
71
|
+
|
|
72
|
+
`auto: true` does not perform filesystem or environment discovery inside deployed
|
|
73
|
+
application code. Project tooling supplies the normalized workspace/profile plan.
|
|
74
|
+
The explicit config-factory constructor is the deterministic semantic reference.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Profile composition token
|
|
78
|
+
|
|
79
|
+
`app.profile()` returns an opaque token intended only for direct passage to a
|
|
80
|
+
composition helper. Package metadata gives each profile fragment one unique
|
|
81
|
+
owner and an explicit helper/re-export ownership vocabulary. Assets and GRIP do
|
|
82
|
+
not yet claim a realized helper, so passing the token to those packages remains
|
|
83
|
+
unsupported until their composition contracts are implemented. The token cannot
|
|
84
|
+
be inspected, stored, destructured, branched on, or returned.
|
|
85
|
+
|
|
86
|
+
## Async-shaped handlers
|
|
87
|
+
|
|
88
|
+
Conventional `.pulse` projects require managed handlers to be async-shaped. A
|
|
89
|
+
synchronous handler is rejected with `PULSE_HANDLER_ASYNC_REQUIRED`. The only
|
|
90
|
+
remaining synchronous-compatibility mode is an explicit migration surface for
|
|
91
|
+
legacy root-config projects and sealed internal compiler evidence.
|
|
92
|
+
|
|
93
|
+
Managed handlers are async-shaped for TypeScript and future explicit JavaScript
|
|
94
|
+
execution:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
app.get('/users/:id', async (ctx) => {
|
|
98
|
+
const user = await ctx
|
|
99
|
+
.fetch(`/origin/users/${ctx.param('id')}`)
|
|
100
|
+
.json<User>('app.User')
|
|
101
|
+
|
|
102
|
+
return ctx.json({ user })
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For native targets, `async` and trusted `await` are authoring notation only:
|
|
107
|
+
|
|
108
|
+
- the async wrapper is erased;
|
|
109
|
+
- awaited Pulse effects lower to the existing effect and continuation state machine;
|
|
110
|
+
- awaiting a synchronous `ctx` surface is erased with a warning;
|
|
111
|
+
- `await next()` remains invalid because `next()` is terminal Router transfer;
|
|
112
|
+
- arbitrary library awaits remain native-ineligible;
|
|
113
|
+
- no Promise runtime, Asyncify, or automatic target fallback is added.
|
|
114
|
+
|
|
115
|
+
## Request state
|
|
116
|
+
|
|
117
|
+
`ctx.state` is a small request-scoped string map:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
app.use(async (ctx, next) => {
|
|
121
|
+
ctx.state.set('request-id', 'r1')
|
|
122
|
+
return next()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
app.get('/health', async (ctx) => {
|
|
126
|
+
return ctx.json({ requestId: ctx.state.get('request-id') })
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
State is shared across the forward Router cursor, mounted Routers, error recovery,
|
|
131
|
+
and native effect suspension/resumption. It is reset for the next request. The
|
|
132
|
+
contract does not expose object-valued state, enumeration, deletion, or persistence.
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
Canonical configuration is one synchronous deferred factory:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { defineConfig } from '@pulse-compute/pulse'
|
|
140
|
+
|
|
141
|
+
export default defineConfig((scope) => ({
|
|
142
|
+
pulse: {
|
|
143
|
+
entry: 'src/index.ts',
|
|
144
|
+
tests: 'tests/pulse.harness.ts',
|
|
145
|
+
defaultProfile: 'dev',
|
|
146
|
+
strict: true,
|
|
147
|
+
},
|
|
148
|
+
dev: {
|
|
149
|
+
host: 'node',
|
|
150
|
+
target: 'native',
|
|
151
|
+
apiBase: scope.config('API_BASE'),
|
|
152
|
+
token: scope.secret('API_TOKEN'),
|
|
153
|
+
},
|
|
154
|
+
}))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The scope is symbolic only. It never exposes resolved values, selected-profile
|
|
158
|
+
state, commands, or ambient environment access.
|
|
159
|
+
|
|
160
|
+
With `pulse.strict: true` (the default), schema-less request JSON is rejected.
|
|
161
|
+
With `pulse.strict: false`, `await ctx.req.json<T>()` selects the byte-bounded
|
|
162
|
+
`host-generic-json` Pulse capability and records its ownership, inclusion reason,
|
|
163
|
+
limit, and dynamic-host cost class in compiler/build metadata. The parser is not
|
|
164
|
+
embedded in the guest Wasm. Declared schema calls remain specialized and validated
|
|
165
|
+
in either mode.
|
|
166
|
+
|
|
167
|
+
## Current boundaries
|
|
168
|
+
|
|
169
|
+
`@pulse-compute/pulse` is the public conventional application surface. It does not expose
|
|
170
|
+
mixed targets or a public compiler/plugin API, and it does not own Handler IR,
|
|
171
|
+
effects, continuations, native lowering, or provider lifecycle.
|
|
172
|
+
|
|
173
|
+
## Shared runtime type contract
|
|
174
|
+
|
|
175
|
+
The current project and execution contracts exclude ambient environment resolution,
|
|
176
|
+
nested profiles, readable active-profile state, runtime-option merging, and
|
|
177
|
+
`listen()` delegation from the wrapper.
|
|
178
|
+
|
|
179
|
+
`@pulse-compute/pulse` now re-exports the **types** owned by
|
|
180
|
+
`@pulse-compute/runtime` for authoring convenience. It does not export a second
|
|
181
|
+
Router value, define another handler/context/effect algebra, or own provider
|
|
182
|
+
lifecycle. The live wrapper uses the same package-internal Router implementation
|
|
183
|
+
while preserving the symbolic configuration factory and opaque profile token. No
|
|
184
|
+
public `handle()`, lifecycle method, or profile introspection is added.
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
## Live wrapper boundary
|
|
188
|
+
|
|
189
|
+
`Pulse` is now backed by the same live Router implementation as `Router`. Its
|
|
190
|
+
application mode and stable opaque profile token are retained in package-private
|
|
191
|
+
state for later tooling and package composition. The wrapper still performs no
|
|
192
|
+
workspace discovery, profile selection, configuration evaluation, or provider
|
|
193
|
+
lifecycle inside application code.
|
package/package.json
CHANGED
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/pulse",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Inert namespace bootstrap for Pulse.",
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
5
4
|
"license": "Apache-2.0",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": "^22.14.0 || ^24.0.0"
|
|
7
|
+
},
|
|
8
|
+
"description": "Project configuration, schema declarations, and the conventional Pulse application root.",
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"main": "./src/index.js",
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.d.ts",
|
|
15
|
+
"require": "./src/index.js",
|
|
16
|
+
"default": "./src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./schema": {
|
|
19
|
+
"types": "./src/schema.d.ts",
|
|
20
|
+
"require": "./src/schema.js",
|
|
21
|
+
"default": "./src/schema.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
6
24
|
"files": [
|
|
25
|
+
"src",
|
|
7
26
|
"README.md",
|
|
8
27
|
"LICENSE",
|
|
9
28
|
"NOTICE"
|
|
10
29
|
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@pulse-compute/runtime": "1.0.0-beta.2",
|
|
32
|
+
"@pulse-compute/wasm-contracts": "1.0.0-beta.2"
|
|
33
|
+
},
|
|
11
34
|
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
|
-
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
40
|
+
"directory": "packages/pulse"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.2/packages/pulse/",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
14
45
|
}
|
|
15
|
-
}
|
|
46
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Router } from '@pulse-compute/runtime';
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
HeaderPair,
|
|
5
|
+
PulseEffect,
|
|
6
|
+
PulseParallelEffect,
|
|
7
|
+
PulseEffectResult,
|
|
8
|
+
PulseParallelResult,
|
|
9
|
+
PulseRequest,
|
|
10
|
+
PulseFetchInit,
|
|
11
|
+
PulseStructuredResponse,
|
|
12
|
+
PulseOpaqueResponse,
|
|
13
|
+
PulseFetchResponse,
|
|
14
|
+
PulseFetchOperation,
|
|
15
|
+
PulseResult,
|
|
16
|
+
PulseResponseOptions,
|
|
17
|
+
PulseJsonResponseOptions,
|
|
18
|
+
PulseState,
|
|
19
|
+
PulseLogger,
|
|
20
|
+
PulseKvNamespace,
|
|
21
|
+
PulseKvGeneration,
|
|
22
|
+
PulseKvVersionedResult,
|
|
23
|
+
PulseKvReadFailureReason,
|
|
24
|
+
PulseKvConditionalResult,
|
|
25
|
+
PulseKvNotStoredReason,
|
|
26
|
+
PulseEmitEvent,
|
|
27
|
+
PulseExecutionContext,
|
|
28
|
+
PulseContext,
|
|
29
|
+
PulseRouteContext,
|
|
30
|
+
PulseEvent,
|
|
31
|
+
PulseEventContext,
|
|
32
|
+
PulseEventHandler,
|
|
33
|
+
HandlerResult,
|
|
34
|
+
Handler,
|
|
35
|
+
RouterNext,
|
|
36
|
+
RouteHandler,
|
|
37
|
+
RouterMiddleware,
|
|
38
|
+
RouterErrorHandler,
|
|
39
|
+
PulseHandlerResult,
|
|
40
|
+
PulseHandler,
|
|
41
|
+
PulseRouteHandler,
|
|
42
|
+
PulseMiddleware,
|
|
43
|
+
PulseErrorHandler
|
|
44
|
+
} from '@pulse-compute/runtime';
|
|
45
|
+
|
|
46
|
+
export type PulseExecutionTarget = 'native' | 'javascript';
|
|
47
|
+
export type PulseReportingLevel = 'off' | 'error' | 'warn' | 'info' | 'debug';
|
|
48
|
+
export type PulseCryptoAlgorithm = 'HS256' | 'ES256' | 'SHA-256' | 'HMAC-SHA256';
|
|
49
|
+
export type PulseCryptoRealization =
|
|
50
|
+
| 'runtime-builtin'
|
|
51
|
+
| 'guest-source:pulse-hmac-as'
|
|
52
|
+
| 'guest-linked:pulse-es256-rustcrypto-p256';
|
|
53
|
+
export type PulseCryptoConfiguration =
|
|
54
|
+
| readonly PulseCryptoAlgorithm[]
|
|
55
|
+
| Readonly<Partial<Record<PulseCryptoAlgorithm, Readonly<{
|
|
56
|
+
realization?: PulseCryptoRealization;
|
|
57
|
+
}>>>>;
|
|
58
|
+
|
|
59
|
+
export interface PulseConfigReference<Name extends string = string> {
|
|
60
|
+
readonly $config: Name;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PulseSecretReference<Name extends string = string> {
|
|
64
|
+
readonly $secret: Name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type PulseSymbolicReference<Name extends string = string> =
|
|
68
|
+
| PulseConfigReference<Name>
|
|
69
|
+
| PulseSecretReference<Name>;
|
|
70
|
+
|
|
71
|
+
export type PulseStaticValue =
|
|
72
|
+
| null
|
|
73
|
+
| string
|
|
74
|
+
| number
|
|
75
|
+
| boolean
|
|
76
|
+
| PulseSymbolicReference
|
|
77
|
+
| readonly PulseStaticValue[]
|
|
78
|
+
| { readonly [key: string]: PulseStaticValue };
|
|
79
|
+
|
|
80
|
+
/** Symbolic configuration authority. It never exposes selected profiles or resolved values. */
|
|
81
|
+
export interface PulseConfigScope {
|
|
82
|
+
config<const Name extends string>(name: Name): PulseConfigReference<Name>;
|
|
83
|
+
secret<const Name extends string>(name: Name): PulseSecretReference<Name>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface PulseProjectOptions {
|
|
87
|
+
/** Workspace-relative application entry. Defaults to src/index.ts. */
|
|
88
|
+
readonly entry?: string;
|
|
89
|
+
/** Optional workspace-relative schema pointer. */
|
|
90
|
+
readonly schema?: string | null;
|
|
91
|
+
/** Optional workspace-relative test harness entry. */
|
|
92
|
+
readonly tests?: string | null;
|
|
93
|
+
/** Fallback after --profile and PULSE_PROFILE. No implicit local profile exists. */
|
|
94
|
+
readonly defaultProfile?: string | null;
|
|
95
|
+
/** Schema-bound request JSON is required when true. Defaults to true. */
|
|
96
|
+
readonly strict?: boolean;
|
|
97
|
+
/** Base log reporting threshold. Defaults to info. */
|
|
98
|
+
readonly reporting?: PulseReportingLevel;
|
|
99
|
+
/** Globally required crypto algorithms. A selected profile declaration replaces this value. */
|
|
100
|
+
readonly crypto?: PulseCryptoConfiguration;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type PulseProfile = Readonly<{
|
|
104
|
+
host: string;
|
|
105
|
+
target: PulseExecutionTarget;
|
|
106
|
+
/** Flat profile override for pulse.reporting. */
|
|
107
|
+
reporting?: PulseReportingLevel;
|
|
108
|
+
/** Required crypto algorithms for this profile. Replaces pulse.crypto rather than merging. */
|
|
109
|
+
crypto?: PulseCryptoConfiguration;
|
|
110
|
+
} & Record<string, PulseStaticValue>>;
|
|
111
|
+
|
|
112
|
+
export type PulseProjectDeclaration = Readonly<{
|
|
113
|
+
pulse?: PulseProjectOptions;
|
|
114
|
+
[profile: string]: PulseProfile | PulseProjectOptions | undefined;
|
|
115
|
+
}>;
|
|
116
|
+
|
|
117
|
+
export interface PulseConfigFactory<out Declaration extends PulseProjectDeclaration = PulseProjectDeclaration> {
|
|
118
|
+
(scope: PulseConfigScope): Declaration;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Preserve and brand one synchronous project/profile declaration factory.
|
|
123
|
+
* The factory is evaluated only with symbolic config and secret references.
|
|
124
|
+
*/
|
|
125
|
+
export declare function defineConfig<const Declaration extends PulseProjectDeclaration>(
|
|
126
|
+
factory: PulseConfigFactory<Declaration>
|
|
127
|
+
): PulseConfigFactory<Declaration>;
|
|
128
|
+
|
|
129
|
+
export interface PulseAutoOptions {
|
|
130
|
+
readonly auto: true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
declare const pulseProfileTokenBrand: unique symbol;
|
|
134
|
+
|
|
135
|
+
/** Opaque project-profile composition token. It has no readable public fields. */
|
|
136
|
+
export interface PulseProfileToken {
|
|
137
|
+
readonly [pulseProfileTokenBrand]: true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type PulseEventDeclaration =
|
|
141
|
+
| Readonly<{ schema: string }>
|
|
142
|
+
| Readonly<{ schema: null }>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Project-aware application root. Pulse shares Router registration and execution
|
|
146
|
+
* semantics; the compiler attaches project/profile metadata beside Router IR.
|
|
147
|
+
*/
|
|
148
|
+
export declare class Pulse extends Router {
|
|
149
|
+
constructor(options: PulseAutoOptions);
|
|
150
|
+
constructor(config: PulseConfigFactory);
|
|
151
|
+
on<Payload = unknown>(
|
|
152
|
+
type: string,
|
|
153
|
+
declaration: Readonly<{ schema: string }>,
|
|
154
|
+
handler: import('@pulse-compute/runtime').PulseEventHandler<Payload>
|
|
155
|
+
): this;
|
|
156
|
+
on(
|
|
157
|
+
type: string,
|
|
158
|
+
declaration: Readonly<{ schema: null }>,
|
|
159
|
+
handler: import('@pulse-compute/runtime').PulseEventHandler<null>
|
|
160
|
+
): this;
|
|
161
|
+
profile(): PulseProfileToken;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export declare const PULSE_APPLICATION_API_VERSION: 'pulse.application-authoring.v3';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { defineConfig } = require('@pulse-compute/wasm-contracts/project/config-factory');
|
|
4
|
+
const { Pulse } = require('./internal/application.js');
|
|
5
|
+
|
|
6
|
+
const PULSE_APPLICATION_API_VERSION = 'pulse.application-authoring.v3';
|
|
7
|
+
|
|
8
|
+
module.exports = Object.freeze({ defineConfig, Pulse, PULSE_APPLICATION_API_VERSION });
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isConfigFactory } = require('@pulse-compute/wasm-contracts/project/config-factory');
|
|
4
|
+
const { Router } = require('@pulse-compute/runtime');
|
|
5
|
+
const {
|
|
6
|
+
addEventRegistration,
|
|
7
|
+
bindEventRegistrationReader,
|
|
8
|
+
createEventRegistrationTable,
|
|
9
|
+
eventRegistrationEntries
|
|
10
|
+
} = require('./event-registration.js');
|
|
11
|
+
|
|
12
|
+
const APPLICATION_STATE = new WeakMap();
|
|
13
|
+
const EVENT_REGISTRATION_STATE = new WeakMap();
|
|
14
|
+
const PROFILE_TOKEN_STATE = new WeakMap();
|
|
15
|
+
const PROFILE_TOKEN_BRAND = Symbol('pulse.profile-token');
|
|
16
|
+
|
|
17
|
+
function isAutoOptions(value) {
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
19
|
+
const keys = Object.keys(value);
|
|
20
|
+
return keys.length === 1 && keys[0] === 'auto' && value.auto === true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class Pulse extends Router {
|
|
24
|
+
constructor(options) {
|
|
25
|
+
super();
|
|
26
|
+
if (!isAutoOptions(options) && !isConfigFactory(options)) {
|
|
27
|
+
throw new TypeError('Pulse requires new Pulse({ auto: true }) or one defineConfig(...) factory.');
|
|
28
|
+
}
|
|
29
|
+
const token = {};
|
|
30
|
+
Object.defineProperty(token, PROFILE_TOKEN_BRAND, { enumerable: false, value: true });
|
|
31
|
+
Object.freeze(token);
|
|
32
|
+
const state = Object.freeze({
|
|
33
|
+
mode: isAutoOptions(options) ? 'auto' : 'explicit',
|
|
34
|
+
options,
|
|
35
|
+
profileToken: token
|
|
36
|
+
});
|
|
37
|
+
APPLICATION_STATE.set(this, state);
|
|
38
|
+
const eventRegistrations = createEventRegistrationTable();
|
|
39
|
+
EVENT_REGISTRATION_STATE.set(this, eventRegistrations);
|
|
40
|
+
bindEventRegistrationReader(this, eventRegistrations);
|
|
41
|
+
PROFILE_TOKEN_STATE.set(token, Object.freeze({ application: this }));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
on(type, declaration, handler) {
|
|
45
|
+
addEventRegistration(EVENT_REGISTRATION_STATE.get(this), type, declaration, handler);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
profile() {
|
|
50
|
+
return APPLICATION_STATE.get(this).profileToken;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function applicationState(application) {
|
|
55
|
+
const state = APPLICATION_STATE.get(application);
|
|
56
|
+
if (!state) throw new TypeError('Expected a Pulse application.');
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function profileTokenOwner(token) {
|
|
61
|
+
return PROFILE_TOKEN_STATE.get(token)?.application;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function eventRegistrations(application) {
|
|
65
|
+
const table = EVENT_REGISTRATION_STATE.get(application);
|
|
66
|
+
if (!table) throw new TypeError('Expected a Pulse application.');
|
|
67
|
+
return eventRegistrationEntries(table);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = Object.freeze({
|
|
71
|
+
Pulse,
|
|
72
|
+
applicationState,
|
|
73
|
+
eventRegistrations,
|
|
74
|
+
profileTokenOwner
|
|
75
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EVENT_REGISTRATION_VERSION = 'pulse.event-registration.v1';
|
|
4
|
+
const EVENT_DECLARATION_VERSION = 'pulse.event-declaration.v1';
|
|
5
|
+
const EVENT_TYPE_MAX_BYTES = 128;
|
|
6
|
+
const EVENT_SCHEMA_ID_MAX_BYTES = 256;
|
|
7
|
+
const SCHEMA_ID_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)+$/;
|
|
8
|
+
const EVENT_REGISTRATION_READER_SYMBOL_KEY = 'pulse.runtime.event-registration-reader.v1';
|
|
9
|
+
const EVENT_REGISTRATION_READER = Symbol.for(EVENT_REGISTRATION_READER_SYMBOL_KEY);
|
|
10
|
+
|
|
11
|
+
const EVENT_REGISTRATION_DIAGNOSTIC_CODES = Object.freeze({
|
|
12
|
+
TYPE_INVALID: 'PULSEWASM_EVENTS_TYPE_INVALID',
|
|
13
|
+
SCHEMA_ID_INVALID: 'PULSEWASM_EVENTS_SCHEMA_ID_INVALID',
|
|
14
|
+
DECLARATION_INVALID: 'PULSEWASM_EVENTS_DECLARATION_INVALID',
|
|
15
|
+
HANDLER_INVALID: 'PULSEWASM_EVENTS_HANDLER_INVALID',
|
|
16
|
+
TYPE_DUPLICATE: 'PULSEWASM_EVENTS_TYPE_DUPLICATE'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function registrationError(code, message, detail = {}) {
|
|
20
|
+
const error = new TypeError(message);
|
|
21
|
+
error.name = 'PulseEventRegistrationError';
|
|
22
|
+
error.code = code;
|
|
23
|
+
error.detail = Object.freeze({ ...detail });
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function utf8ByteLength(value) {
|
|
28
|
+
const text = String(value);
|
|
29
|
+
let bytes = 0;
|
|
30
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
31
|
+
const point = text.codePointAt(index);
|
|
32
|
+
if (point <= 0x7f) bytes += 1;
|
|
33
|
+
else if (point <= 0x7ff) bytes += 2;
|
|
34
|
+
else if (point <= 0xffff) bytes += 3;
|
|
35
|
+
else {
|
|
36
|
+
bytes += 4;
|
|
37
|
+
index += 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return bytes;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requireDataDeclaration(value) {
|
|
44
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
45
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.DECLARATION_INVALID, 'Pulse.on declaration must be a plain object containing only schema.');
|
|
46
|
+
}
|
|
47
|
+
const prototype = Object.getPrototypeOf(value);
|
|
48
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
49
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.DECLARATION_INVALID, 'Pulse.on declaration must be a plain object containing only schema.');
|
|
50
|
+
}
|
|
51
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
52
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.DECLARATION_INVALID, 'Pulse.on declaration must not contain symbol keys.');
|
|
53
|
+
}
|
|
54
|
+
const keys = Object.keys(value);
|
|
55
|
+
if (keys.length !== 1 || keys[0] !== 'schema') {
|
|
56
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.DECLARATION_INVALID, 'Pulse.on declaration must contain exactly one schema data property.', { fields: keys.sort() });
|
|
57
|
+
}
|
|
58
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, 'schema');
|
|
59
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
60
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.DECLARATION_INVALID, 'Pulse.on declaration.schema must be an own data property.');
|
|
61
|
+
}
|
|
62
|
+
return descriptor.value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeEventType(value) {
|
|
66
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
67
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.TYPE_INVALID, 'Pulse.on event type must be a non-empty string.', { value });
|
|
68
|
+
}
|
|
69
|
+
const bytes = utf8ByteLength(value);
|
|
70
|
+
if (bytes > EVENT_TYPE_MAX_BYTES) {
|
|
71
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.TYPE_INVALID, 'Pulse.on event type exceeds its UTF-8 byte limit.', { bytes, maxBytes: EVENT_TYPE_MAX_BYTES });
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeEventDeclaration(value) {
|
|
77
|
+
const schema = requireDataDeclaration(value);
|
|
78
|
+
if (schema !== null && (typeof schema !== 'string' || !SCHEMA_ID_PATTERN.test(schema) || utf8ByteLength(schema) > EVENT_SCHEMA_ID_MAX_BYTES)) {
|
|
79
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.SCHEMA_ID_INVALID, 'Pulse.on schema must be a bounded dotted schema ID or null.', { schema });
|
|
80
|
+
}
|
|
81
|
+
return Object.freeze({ version: EVENT_DECLARATION_VERSION, schemaId: schema });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeEventRegistration(typeInput, declarationInput, handler) {
|
|
85
|
+
const type = normalizeEventType(typeInput);
|
|
86
|
+
const declaration = normalizeEventDeclaration(declarationInput);
|
|
87
|
+
if (typeof handler !== 'function') {
|
|
88
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.HANDLER_INVALID, 'Pulse.on requires an event handler function.');
|
|
89
|
+
}
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
version: EVENT_REGISTRATION_VERSION,
|
|
92
|
+
type,
|
|
93
|
+
declaration,
|
|
94
|
+
handler
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createEventRegistrationTable() {
|
|
99
|
+
return { entries: [], types: new Set() };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addEventRegistration(table, type, declaration, handler) {
|
|
103
|
+
const registration = normalizeEventRegistration(type, declaration, handler);
|
|
104
|
+
if (table.types.has(registration.type)) {
|
|
105
|
+
throw registrationError(EVENT_REGISTRATION_DIAGNOSTIC_CODES.TYPE_DUPLICATE, `Pulse event type ${JSON.stringify(registration.type)} is already registered.`, { type: registration.type });
|
|
106
|
+
}
|
|
107
|
+
table.types.add(registration.type);
|
|
108
|
+
table.entries.push(registration);
|
|
109
|
+
return registration;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function eventRegistrationEntries(table) {
|
|
113
|
+
return Object.freeze([...table.entries]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function bindEventRegistrationReader(application, table) {
|
|
117
|
+
Object.defineProperty(application, EVENT_REGISTRATION_READER, {
|
|
118
|
+
enumerable: false,
|
|
119
|
+
configurable: false,
|
|
120
|
+
writable: false,
|
|
121
|
+
value: () => eventRegistrationEntries(table)
|
|
122
|
+
});
|
|
123
|
+
return application;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = Object.freeze({
|
|
127
|
+
EVENT_REGISTRATION_VERSION,
|
|
128
|
+
EVENT_DECLARATION_VERSION,
|
|
129
|
+
EVENT_TYPE_MAX_BYTES,
|
|
130
|
+
EVENT_SCHEMA_ID_MAX_BYTES,
|
|
131
|
+
EVENT_REGISTRATION_READER_SYMBOL_KEY,
|
|
132
|
+
EVENT_REGISTRATION_DIAGNOSTIC_CODES,
|
|
133
|
+
normalizeEventType,
|
|
134
|
+
normalizeEventDeclaration,
|
|
135
|
+
normalizeEventRegistration,
|
|
136
|
+
createEventRegistrationTable,
|
|
137
|
+
addEventRegistration,
|
|
138
|
+
eventRegistrationEntries,
|
|
139
|
+
bindEventRegistrationReader
|
|
140
|
+
});
|
package/src/schema.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type Int32 = number & { readonly __pulseInt32?: never };
|
|
2
|
+
export type Uint32 = number & { readonly __pulseUint32?: never };
|
|
3
|
+
|
|
4
|
+
declare const schemaDeclarationBrand: unique symbol;
|
|
5
|
+
declare const responseCaseDeclarationBrand: unique symbol;
|
|
6
|
+
declare const schemaRegistryBrand: unique symbol;
|
|
7
|
+
|
|
8
|
+
export interface SchemaDeclaration<Type> {
|
|
9
|
+
readonly [schemaDeclarationBrand]: Type;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ResponseCaseDeclaration<
|
|
13
|
+
Status extends number = number,
|
|
14
|
+
SchemaId extends string = string
|
|
15
|
+
> {
|
|
16
|
+
readonly status: Status;
|
|
17
|
+
readonly schemaId: SchemaId;
|
|
18
|
+
readonly [responseCaseDeclarationBrand]: true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SchemaRegistryDeclaration<
|
|
22
|
+
Schemas extends Readonly<Record<string, SchemaDeclaration<unknown>>>,
|
|
23
|
+
Responses extends Readonly<Record<string, ResponseCaseDeclaration>>
|
|
24
|
+
> {
|
|
25
|
+
readonly schemas: Schemas;
|
|
26
|
+
readonly responses?: Responses;
|
|
27
|
+
readonly [schemaRegistryBrand]: true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Declare one required object-root schema. The stable string key is authoritative. */
|
|
31
|
+
export declare function schema<Type>(): SchemaDeclaration<Type>;
|
|
32
|
+
|
|
33
|
+
/** Map one semantic response-case ID to an HTTP status and registered schema ID. */
|
|
34
|
+
export declare function response<const Status extends number, const SchemaId extends string>(
|
|
35
|
+
status: Status,
|
|
36
|
+
schemaId: SchemaId
|
|
37
|
+
): ResponseCaseDeclaration<Status, SchemaId>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Preserve one static schema registry. Pulse extracts this default-exported call;
|
|
41
|
+
* it does not execute application registry code during compilation.
|
|
42
|
+
*/
|
|
43
|
+
export declare function defineSchemaRegistry<
|
|
44
|
+
const Schemas extends Readonly<Record<string, SchemaDeclaration<unknown>>>,
|
|
45
|
+
const Responses extends Readonly<Record<string, ResponseCaseDeclaration>>
|
|
46
|
+
>(registry: {
|
|
47
|
+
readonly schemas: Schemas;
|
|
48
|
+
readonly responses?: Responses;
|
|
49
|
+
}): SchemaRegistryDeclaration<Schemas, Responses>;
|
|
50
|
+
|
|
51
|
+
export declare const SCHEMA_AUTHORING_VERSION: 'pulse.schema-authoring.v1';
|
package/src/schema.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SCHEMA_AUTHORING_VERSION = 'pulse.schema-authoring.v1';
|
|
4
|
+
const SCHEMA_DECLARATION_BRAND = Symbol.for('pulse.schema-declaration.v1');
|
|
5
|
+
const RESPONSE_CASE_DECLARATION_BRAND = Symbol.for('pulse.response-case-declaration.v1');
|
|
6
|
+
const SCHEMA_REGISTRY_BRAND = Symbol.for('pulse.schema-registry.v1');
|
|
7
|
+
|
|
8
|
+
function schema() {
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
version: SCHEMA_AUTHORING_VERSION,
|
|
11
|
+
[SCHEMA_DECLARATION_BRAND]: true
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function response(status, schemaId) {
|
|
16
|
+
if (!Number.isSafeInteger(status) || status < 100 || status > 599) {
|
|
17
|
+
throw new TypeError('response(status, schemaId) requires an HTTP status from 100 through 599.');
|
|
18
|
+
}
|
|
19
|
+
if (typeof schemaId !== 'string' || schemaId.trim() === '') {
|
|
20
|
+
throw new TypeError('response(status, schemaId) requires a non-empty schema ID.');
|
|
21
|
+
}
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
version: SCHEMA_AUTHORING_VERSION,
|
|
24
|
+
status,
|
|
25
|
+
schemaId: schemaId.trim(),
|
|
26
|
+
[RESPONSE_CASE_DECLARATION_BRAND]: true
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function defineSchemaRegistry(registry) {
|
|
31
|
+
if (!registry || typeof registry !== 'object' || Array.isArray(registry)) {
|
|
32
|
+
throw new TypeError('defineSchemaRegistry requires a static registry object.');
|
|
33
|
+
}
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
...registry,
|
|
36
|
+
[SCHEMA_REGISTRY_BRAND]: SCHEMA_AUTHORING_VERSION
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = Object.freeze({
|
|
41
|
+
SCHEMA_AUTHORING_VERSION,
|
|
42
|
+
defineSchemaRegistry,
|
|
43
|
+
schema,
|
|
44
|
+
response
|
|
45
|
+
});
|