@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 CHANGED
@@ -1,3 +1,193 @@
1
1
  # @pulse-compute/pulse
2
2
 
3
- This is an inert namespace-bootstrap package. It contains no executable code.
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.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
- "tag": "bootstrap"
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
+ });
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./application.js');
@@ -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
+ });