@pulse-compute/cli 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.
Files changed (191) hide show
  1. package/API.md +572 -0
  2. package/CHANGELOG.md +71 -0
  3. package/README.md +71 -1
  4. package/bin/pulse.js +15 -0
  5. package/bin/pulsewasm-extract.js +22 -0
  6. package/cli-spec.json +575 -0
  7. package/completions/_pulse +106 -0
  8. package/completions/pulse.bash +31 -0
  9. package/completions/pulse.fish +76 -0
  10. package/docs/README.md +77 -0
  11. package/docs/architecture/current-contracts.md +470 -0
  12. package/docs/architecture/overview.md +93 -0
  13. package/docs/architecture/vision.md +515 -0
  14. package/docs/concepts/bodies.md +186 -0
  15. package/docs/concepts/compilation-and-lowering.md +496 -0
  16. package/docs/concepts/contracts-and-providers.md +158 -0
  17. package/docs/concepts/effects-and-continuations.md +300 -0
  18. package/docs/concepts/entities-and-adapters.md +71 -0
  19. package/docs/concepts/package-owned-lowering.md +175 -0
  20. package/docs/concepts/targets-and-hosts.md +70 -0
  21. package/docs/contributing/README.md +38 -0
  22. package/docs/contributing/adding-core-provider.md +315 -0
  23. package/docs/contributing/adding-first-party-lowerer.md +327 -0
  24. package/docs/contributing/entities-lowering.md +91 -0
  25. package/docs/contributing/package-lowerer-contract.md +383 -0
  26. package/docs/contributing/pulse-aware-packages.md +149 -0
  27. package/docs/examples.md +112 -0
  28. package/docs/fixtures/inspect-fetch-composition.selected.json +181 -0
  29. package/docs/getting-started.md +128 -0
  30. package/docs/guides/compatibility-imports.md +60 -0
  31. package/docs/guides/deploying-fastly.md +188 -0
  32. package/docs/guides/deploying-node.md +143 -0
  33. package/docs/guides/events.md +233 -0
  34. package/docs/guides/fastly-capabilities.md +153 -0
  35. package/docs/guides/fetching-and-composition.md +101 -0
  36. package/docs/guides/grip.md +94 -0
  37. package/docs/guides/json-schemas.md +233 -0
  38. package/docs/guides/migrating-from-express.md +192 -0
  39. package/docs/guides/project-lifecycle.md +143 -0
  40. package/docs/guides/routing.md +202 -0
  41. package/docs/guides/troubleshooting.md +47 -0
  42. package/docs/maintainers/README.md +60 -0
  43. package/docs/maintainers/codex-maintainer.md +71 -0
  44. package/docs/maintainers/documentation-deployment.md +147 -0
  45. package/docs/maintainers/documentation-system.md +58 -0
  46. package/docs/maintainers/documentation-versioning.md +65 -0
  47. package/docs/maintainers/maintainer-charter.md +100 -0
  48. package/docs/maintainers/maintenance-policy.json +809 -0
  49. package/docs/maintainers/maintenance-policy.md +124 -0
  50. package/docs/maintainers/npm-publishing.md +154 -0
  51. package/docs/maintainers/plugin-readiness.json +19 -0
  52. package/docs/maintainers/plugin-readiness.md +31 -0
  53. package/docs/maintainers/public-site.md +158 -0
  54. package/docs/maintainers/release-acceptance.md +293 -0
  55. package/docs/maintainers/release-manifest.md +89 -0
  56. package/docs/maintainers/repository-setup.md +145 -0
  57. package/docs/maintainers/scope-policy.md +105 -0
  58. package/docs/maintainers/support-and-triage.md +74 -0
  59. package/docs/maintainers/testing.md +334 -0
  60. package/docs/packages/README.md +60 -0
  61. package/docs/packages/assets.md +85 -0
  62. package/docs/packages/cli.md +190 -0
  63. package/docs/packages/crypto.md +78 -0
  64. package/docs/packages/entities.md +129 -0
  65. package/docs/packages/grip.md +82 -0
  66. package/docs/packages/implementation-packages.md +122 -0
  67. package/docs/packages/jwt.md +55 -0
  68. package/docs/packages/provider-fastly.md +243 -0
  69. package/docs/packages/pulse.md +159 -0
  70. package/docs/packages/runtime.md +265 -0
  71. package/docs/packages/s3.md +101 -0
  72. package/docs/preview-scope.md +142 -0
  73. package/docs/reference/README.md +33 -0
  74. package/docs/reference/cli-spec.json +575 -0
  75. package/docs/reference/cli.md +457 -0
  76. package/docs/reference/compatibility-matrix.md +169 -0
  77. package/docs/reference/diagnostics.md +1872 -0
  78. package/docs/reference/documentation-versions.json +22 -0
  79. package/docs/reference/environment.md +347 -0
  80. package/docs/reference/handler-authoring.md +264 -0
  81. package/docs/reference/project-config.md +582 -0
  82. package/docs/reference/project-config.schema.json +981 -0
  83. package/docs/reference/release-manifest.json +441 -0
  84. package/docs/reference/shell-completion.md +23 -0
  85. package/documentation-site.json +668 -0
  86. package/documentation-versions.json +22 -0
  87. package/examples/01-hello-json/.pulse/.gitignore +3 -0
  88. package/examples/01-hello-json/.pulse/config.ts +16 -0
  89. package/examples/01-hello-json/README.md +119 -0
  90. package/examples/01-hello-json/package.json +20 -0
  91. package/examples/01-hello-json/src/index.ts +9 -0
  92. package/examples/01-hello-json/tests/pulse.harness.ts +22 -0
  93. package/examples/01-hello-json/tsconfig.json +14 -0
  94. package/examples/02-request-schema/.pulse/.gitignore +3 -0
  95. package/examples/02-request-schema/.pulse/config.ts +17 -0
  96. package/examples/02-request-schema/README.md +145 -0
  97. package/examples/02-request-schema/package.json +20 -0
  98. package/examples/02-request-schema/src/index.ts +18 -0
  99. package/examples/02-request-schema/src/schemas.ts +20 -0
  100. package/examples/02-request-schema/tests/pulse.harness.ts +37 -0
  101. package/examples/02-request-schema/tsconfig.json +14 -0
  102. package/examples/03-fetch-composition/.pulse/.gitignore +3 -0
  103. package/examples/03-fetch-composition/.pulse/config.ts +31 -0
  104. package/examples/03-fetch-composition/README.md +210 -0
  105. package/examples/03-fetch-composition/package.json +20 -0
  106. package/examples/03-fetch-composition/src/index.ts +60 -0
  107. package/examples/03-fetch-composition/tests/pulse.harness.ts +47 -0
  108. package/examples/03-fetch-composition/tsconfig.json +14 -0
  109. package/examples/05-fastly-capabilities/.pulse/.gitignore +3 -0
  110. package/examples/05-fastly-capabilities/.pulse/config.ts +57 -0
  111. package/examples/05-fastly-capabilities/README.md +257 -0
  112. package/examples/05-fastly-capabilities/package.json +22 -0
  113. package/examples/05-fastly-capabilities/src/index.ts +55 -0
  114. package/examples/05-fastly-capabilities/tests/pulse.harness.ts +66 -0
  115. package/examples/05-fastly-capabilities/tsconfig.json +14 -0
  116. package/examples/07-opaque-proxy/.pulse/.gitignore +3 -0
  117. package/examples/07-opaque-proxy/.pulse/config.ts +38 -0
  118. package/examples/07-opaque-proxy/README.md +150 -0
  119. package/examples/07-opaque-proxy/package.json +21 -0
  120. package/examples/07-opaque-proxy/src/index.ts +9 -0
  121. package/examples/07-opaque-proxy/tests/pulse.harness.ts +25 -0
  122. package/examples/07-opaque-proxy/tsconfig.json +14 -0
  123. package/examples/09-router-lowering/.pulse/.gitignore +3 -0
  124. package/examples/09-router-lowering/.pulse/config.ts +22 -0
  125. package/examples/09-router-lowering/README.md +287 -0
  126. package/examples/09-router-lowering/package.json +22 -0
  127. package/examples/09-router-lowering/src/index.ts +65 -0
  128. package/examples/09-router-lowering/tests/pulse.harness.ts +91 -0
  129. package/examples/09-router-lowering/tsconfig.json +14 -0
  130. package/examples/10-entities-tools/.pulse/.gitignore +4 -0
  131. package/examples/10-entities-tools/.pulse/config.ts +23 -0
  132. package/examples/10-entities-tools/README.md +165 -0
  133. package/examples/10-entities-tools/package.json +21 -0
  134. package/examples/10-entities-tools/src/handlers.ts +15 -0
  135. package/examples/10-entities-tools/src/index.ts +31 -0
  136. package/examples/10-entities-tools/src/schemas.ts +18 -0
  137. package/examples/10-entities-tools/tests/pulse.harness.ts +40 -0
  138. package/examples/10-entities-tools/tools-facade.cjs +158 -0
  139. package/examples/10-entities-tools/tsconfig.json +14 -0
  140. package/examples/11-events/.pulse/.gitignore +4 -0
  141. package/examples/11-events/.pulse/config.ts +24 -0
  142. package/examples/11-events/README.md +194 -0
  143. package/examples/11-events/package.json +20 -0
  144. package/examples/11-events/src/index.ts +27 -0
  145. package/examples/11-events/src/schemas.ts +19 -0
  146. package/examples/11-events/tests/pulse.harness.ts +34 -0
  147. package/examples/11-events/tsconfig.json +15 -0
  148. package/examples/12-mcp-proxy/.pulse/.gitignore +3 -0
  149. package/examples/12-mcp-proxy/.pulse/config.ts +25 -0
  150. package/examples/12-mcp-proxy/README.md +149 -0
  151. package/examples/12-mcp-proxy/package.json +20 -0
  152. package/examples/12-mcp-proxy/src/index.ts +17 -0
  153. package/examples/12-mcp-proxy/tests/pulse.harness.ts +29 -0
  154. package/examples/12-mcp-proxy/tsconfig.json +14 -0
  155. package/examples/13-jwt-es256/.pulse/config.ts +19 -0
  156. package/examples/13-jwt-es256/README.md +195 -0
  157. package/examples/13-jwt-es256/package.json +21 -0
  158. package/examples/13-jwt-es256/src/index.ts +21 -0
  159. package/examples/13-jwt-es256/tests/pulse.harness.ts +54 -0
  160. package/examples/13-jwt-es256/tsconfig.json +14 -0
  161. package/examples/README.md +36 -0
  162. package/package.json +66 -6
  163. package/project-config.schema.json +981 -0
  164. package/release-manifest.json +441 -0
  165. package/src/command-spec.js +279 -0
  166. package/src/completion.js +113 -0
  167. package/src/diagnostics.js +350 -0
  168. package/src/documentation.js +45 -0
  169. package/src/index.d.ts +133 -0
  170. package/src/index.js +15 -0
  171. package/src/internal/command-executor.d.ts +29 -0
  172. package/src/internal/command-executor.js +143 -0
  173. package/src/internal/command-plan.d.ts +9 -0
  174. package/src/internal/command-plan.js +73 -0
  175. package/src/internal/command-reporter.d.ts +15 -0
  176. package/src/internal/command-reporter.js +133 -0
  177. package/src/internal/command-request.d.ts +35 -0
  178. package/src/internal/command-request.js +154 -0
  179. package/src/internal/node-http.js +54 -0
  180. package/src/internal/project-context.d.ts +66 -0
  181. package/src/internal/project-context.js +175 -0
  182. package/src/project-config-reference.js +4 -0
  183. package/src/project-config-schema.d.ts +23 -0
  184. package/src/project-config-schema.js +271 -0
  185. package/src/project-config.js +724 -0
  186. package/src/project-execution.js +2946 -0
  187. package/src/provider-drivers.js +3 -0
  188. package/src/target-support.js +3 -0
  189. package/src/typescript-module-loader.js +269 -0
  190. package/src/workflow.js +78 -0
  191. package/src/workspace.js +82 -0
@@ -0,0 +1,15 @@
1
+ type CustomerLookupInput = Readonly<{
2
+ email: string
3
+ }>
4
+
5
+ export async function systemStatus(_ctx: unknown, _input: undefined) {
6
+ return undefined
7
+ }
8
+
9
+ export async function lookupCustomer(ctx: any, input: CustomerLookupInput) {
10
+ const displayName = await ctx.fetch(
11
+ `https://directory.example.test/customers/${input.email}`,
12
+ ).text()
13
+ return { email: input.email, displayName }
14
+ }
15
+
@@ -0,0 +1,31 @@
1
+ import { EntityRouter, jsonRpc } from '@pulse-compute/entities'
2
+ import { lookupCustomer, systemStatus } from './handlers.js'
3
+
4
+ const rpc = new EntityRouter({
5
+ adapter: jsonRpc({ namedParamsOnly: true, acceptEmptyObjectForNoInput: true }),
6
+ })
7
+
8
+ rpc.on('system.status', {
9
+ input: null,
10
+ output: null,
11
+ metadata: {
12
+ title: 'Check system status',
13
+ description: 'Checks that the governed entity request boundary is available.',
14
+ mcp: { readOnlyHint: true },
15
+ },
16
+ }, systemStatus)
17
+
18
+ rpc.on('customer.lookup', {
19
+ input: 'tools.CustomerLookupInput',
20
+ output: 'tools.CustomerLookupOutput',
21
+ metadata: {
22
+ title: 'Look up customer',
23
+ description: 'Looks up one customer through the configured directory backend.',
24
+ mcp: { readOnlyHint: true },
25
+ },
26
+ }, lookupCustomer)
27
+
28
+ export default async function handler(ctx: unknown) {
29
+ return rpc.handle(ctx as never)
30
+ }
31
+
@@ -0,0 +1,18 @@
1
+ import { defineSchemaRegistry, schema } from '@pulse-compute/pulse/schema'
2
+
3
+ export interface CustomerLookupInput {
4
+ email: string
5
+ }
6
+
7
+ export interface CustomerLookupOutput {
8
+ email: string
9
+ displayName: string
10
+ }
11
+
12
+ export default defineSchemaRegistry({
13
+ schemas: {
14
+ 'tools.CustomerLookupInput': schema<CustomerLookupInput>(),
15
+ 'tools.CustomerLookupOutput': schema<CustomerLookupOutput>(),
16
+ },
17
+ })
18
+
@@ -0,0 +1,40 @@
1
+ export default { cases: [
2
+ {
3
+ name: 'system-status',
4
+ request: {
5
+ method: 'POST',
6
+ path: '/',
7
+ headers: { 'content-type': 'application/json' },
8
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'system.status', id: 'status-doc' }),
9
+ },
10
+ expect: {
11
+ status: 200,
12
+ json: { jsonrpc: '2.0', result: null, id: 'status-doc' },
13
+ },
14
+ },
15
+ {
16
+ name: 'customer-lookup',
17
+ request: {
18
+ method: 'POST',
19
+ path: '/',
20
+ headers: { 'content-type': 'application/json' },
21
+ body: JSON.stringify({
22
+ jsonrpc: '2.0',
23
+ method: 'customer.lookup',
24
+ params: { email: 'ada@example.test' },
25
+ id: 'lookup-doc',
26
+ }),
27
+ },
28
+ fetches: {
29
+ 'https://directory.example.test/customers/ada@example.test': { value: 'Ada Lovelace' },
30
+ },
31
+ expect: {
32
+ status: 200,
33
+ json: {
34
+ jsonrpc: '2.0',
35
+ result: { email: 'ada@example.test', displayName: 'Ada Lovelace' },
36
+ id: 'lookup-doc',
37
+ },
38
+ },
39
+ },
40
+ ] }
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const TOOLS_FACADE_VERSION = 'pulse.entities-tools-facade-demo.v1';
4
+ const TOOLS_CATALOG_VERSION = 'pulse.entities-tools-catalog-demo.v1';
5
+
6
+ const TOOLS_FACADE_POLICY = Object.freeze({
7
+ completeMcpServer: false,
8
+ discovery: 'static-entity-catalog',
9
+ invocation: 'json-rpc-2.0-request-boundary',
10
+ governedExecution: 'pulse-entity-handler',
11
+ excluded: Object.freeze([
12
+ 'sse',
13
+ 'sessions',
14
+ 'tasks',
15
+ 'resources',
16
+ 'prompts',
17
+ 'sampling',
18
+ 'authorization',
19
+ 'transport-negotiation',
20
+ 'runtime-core-protocol-state'
21
+ ])
22
+ });
23
+
24
+ class ToolsFacadeInvocationError extends Error {
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.name = 'ToolsFacadeInvocationError';
28
+ this.code = code;
29
+ }
30
+ }
31
+
32
+ function staticMetadata(value) {
33
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : Object.freeze({});
34
+ }
35
+
36
+ function projectTool(entity) {
37
+ const metadata = staticMetadata(entity.metadata);
38
+ const hints = staticMetadata(metadata.mcp);
39
+ return Object.freeze({
40
+ name: entity.name,
41
+ title: typeof metadata.title === 'string' ? metadata.title : entity.name,
42
+ description: typeof metadata.description === 'string' ? metadata.description : '',
43
+ inputSchemaId: entity.inputSchema,
44
+ outputSchemaId: entity.outputSchema,
45
+ annotations: Object.freeze({ readOnlyHint: hints.readOnlyHint === true })
46
+ });
47
+ }
48
+
49
+ function readTools(catalog) {
50
+ if (!catalog || catalog.version !== 'pulse.entities-catalog.v1') {
51
+ throw new TypeError('Tools facade requires a pulse.entities-catalog.v1 artifact.');
52
+ }
53
+ if (!Array.isArray(catalog.routers) || catalog.routers.length !== 1) {
54
+ throw new TypeError('Tools facade demonstration requires exactly one static entity router.');
55
+ }
56
+ const router = catalog.routers[0];
57
+ if (!router || router.adapter !== 'json-rpc' || !Array.isArray(router.entities)) {
58
+ throw new TypeError('Tools facade demonstration requires one JSON-RPC entity router.');
59
+ }
60
+ const tools = router.entities.map(projectTool);
61
+ if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
62
+ throw new TypeError('Tools facade catalog contains duplicate tool names.');
63
+ }
64
+ return Object.freeze(tools);
65
+ }
66
+
67
+ function requestId(value) {
68
+ if (value === null || typeof value === 'string' || Number.isFinite(value)) return value;
69
+ throw new TypeError('Tools facade request ID must be a string, finite number, or null.');
70
+ }
71
+
72
+ function createToolsFacade({ catalog, invoke }) {
73
+ if (typeof invoke !== 'function') {
74
+ throw new TypeError(
75
+ 'Tools facade requires an injected request-boundary invoker.'
76
+ );
77
+ }
78
+ const tools = readTools(catalog);
79
+ const toolsByName = new Map(tools.map((tool) => [tool.name, tool]));
80
+
81
+ return Object.freeze({
82
+ version: TOOLS_FACADE_VERSION,
83
+ policy: TOOLS_FACADE_POLICY,
84
+ listTools() {
85
+ return Object.freeze({ version: TOOLS_CATALOG_VERSION, tools });
86
+ },
87
+ async callTool(name, input, options = {}) {
88
+ const tool = toolsByName.get(name);
89
+ if (!tool) throw new RangeError(`Unknown catalog tool ${String(name)}.`);
90
+ if (
91
+ tool.inputSchemaId !== null
92
+ && (input === null || typeof input !== 'object' || Array.isArray(input))
93
+ ) {
94
+ throw new TypeError(`Tool ${name} requires a named object input.`);
95
+ }
96
+ if (tool.inputSchemaId === null && input !== undefined) {
97
+ throw new TypeError(`Tool ${name} does not accept input.`);
98
+ }
99
+ const envelope = {
100
+ jsonrpc: '2.0',
101
+ method: name,
102
+ ...(tool.inputSchemaId === null ? {} : { params: input }),
103
+ id: requestId(options.id === undefined ? `tools:${name}` : options.id)
104
+ };
105
+ const response = await invoke(Object.freeze({
106
+ method: 'POST',
107
+ headers: Object.freeze([Object.freeze(['content-type', 'application/json'])]),
108
+ body: JSON.stringify(envelope)
109
+ }));
110
+ const status = Number(response && response.status);
111
+ const body = response && typeof response.body === 'string' ? response.body : '';
112
+ if (status !== 200 || body.length === 0) {
113
+ throw new ToolsFacadeInvocationError(
114
+ 'PULSE_TOOLS_BOUNDARY_RESPONSE_INVALID',
115
+ 'Tool request boundary returned an invalid response.'
116
+ );
117
+ }
118
+ let payload;
119
+ try { payload = JSON.parse(body); }
120
+ catch (_) {
121
+ throw new ToolsFacadeInvocationError(
122
+ 'PULSE_TOOLS_BOUNDARY_JSON_INVALID',
123
+ 'Tool request boundary returned invalid JSON.'
124
+ );
125
+ }
126
+ if (
127
+ !payload
128
+ || payload.jsonrpc !== '2.0'
129
+ || JSON.stringify(payload.id) !== JSON.stringify(envelope.id)
130
+ ) {
131
+ throw new ToolsFacadeInvocationError(
132
+ 'PULSE_TOOLS_BOUNDARY_ENVELOPE_INVALID',
133
+ 'Tool request boundary returned an invalid JSON-RPC envelope.'
134
+ );
135
+ }
136
+ if (payload.error) {
137
+ throw new ToolsFacadeInvocationError(
138
+ 'PULSE_TOOLS_CALL_FAILED',
139
+ typeof payload.error.message === 'string' ? payload.error.message : 'Tool call failed.'
140
+ );
141
+ }
142
+ if (!Object.prototype.hasOwnProperty.call(payload, 'result')) {
143
+ throw new ToolsFacadeInvocationError(
144
+ 'PULSE_TOOLS_BOUNDARY_RESULT_MISSING',
145
+ 'Tool request boundary omitted the result.'
146
+ );
147
+ }
148
+ return payload.result;
149
+ }
150
+ });
151
+ }
152
+
153
+ module.exports = Object.freeze({
154
+ TOOLS_FACADE_POLICY,
155
+ TOOLS_FACADE_VERSION,
156
+ ToolsFacadeInvocationError,
157
+ createToolsFacade
158
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noEmit": true
8
+ },
9
+ "include": [
10
+ "src/**/*.ts",
11
+ ".pulse/**/*.ts"
12
+ ]
13
+ }
14
+
@@ -0,0 +1,4 @@
1
+ *
2
+ !.gitignore
3
+ !config.ts
4
+
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from '@pulse-compute/pulse'
2
+
3
+ export default defineConfig((_scope) => ({
4
+ pulse: {
5
+ entry: 'src/index.ts',
6
+ schema: 'src/schemas.ts',
7
+ tests: 'tests/pulse.harness.ts',
8
+ defaultProfile: 'node-native',
9
+ strict: true,
10
+ },
11
+ 'node-native': {
12
+ host: 'node',
13
+ target: 'native',
14
+ outDir: 'dist-node-native',
15
+ dev: { host: '127.0.0.1', port: 8787, networkFetch: false },
16
+ },
17
+ 'node-javascript': {
18
+ host: 'node',
19
+ target: 'javascript',
20
+ outDir: 'dist-node-javascript',
21
+ dev: { host: '127.0.0.1', port: 8787, networkFetch: false },
22
+ },
23
+ }))
24
+
@@ -0,0 +1,194 @@
1
+ # Static events
2
+
3
+ This mixed Node project keeps HTTP and event entries in separate planes. Two
4
+ schema-bound `Pulse.on` registrations accept canonical event frames and use the
5
+ one-way `ctx.emit` effect. The harness proves exact outbound acceptance; it
6
+ does not claim delivery or invoke a matching local handler.
7
+
8
+ ## Workflow
9
+
10
+ ```bash
11
+ pulse doctor
12
+ pulse inspect
13
+ pulse test
14
+ pulse dev
15
+ pulse build
16
+ ```
17
+
18
+ `pulse test` executes both HTTP and event cases on the selected Node target.
19
+ `pulse dev` serves only the HTTP portion; Pulse does not expose a public event
20
+ listener or injection command.
21
+
22
+ The documented result below is executed by the documentation gate.
23
+
24
+ <!-- pulse-doc-run {"project":"examples/11-events","args":["test","--json"],"timeoutMs":240000} -->
25
+ ```bash
26
+ pulse test --json
27
+ ```
28
+ ```json
29
+ {
30
+ "status": "passed",
31
+ "provider": "node",
32
+ "summary": {
33
+ "total": 3,
34
+ "passed": 3,
35
+ "failed": 0
36
+ },
37
+ "cases": [
38
+ {
39
+ "name": "http health remains separate",
40
+ "status": "passed",
41
+ "response": {
42
+ "status": 200
43
+ }
44
+ },
45
+ {
46
+ "name": "schema event emits an accepted frame",
47
+ "status": "passed",
48
+ "kind": "event",
49
+ "emittedFrameCount": 1
50
+ }
51
+ ]
52
+ }
53
+ ```
54
+
55
+ ## Wasm size
56
+
57
+ | Artifact | Default build | `--experimental-native-size` | Reduction |
58
+ |---|---:|---:|---:|
59
+ | `canonical-native.wasm` | 38.4 KiB (39,358 bytes) | 30.6 KiB (31,313 bytes) | 20.4% |
60
+
61
+ The executable documentation gate rebuilds and measures both variants on
62
+ `1.0.0-beta.2`. These are uncompressed on-disk sizes, not transfer sizes or
63
+ platform limits.
64
+
65
+ ## Application
66
+
67
+ <!-- pulse-doc-source: examples/11-events/src/index.ts -->
68
+ ```ts
69
+ import { Pulse } from '@pulse-compute/pulse'
70
+
71
+ type DeviceReading = Readonly<{
72
+ deviceId: string
73
+ temperatureC: number
74
+ }>
75
+
76
+ const app = new Pulse({ auto: true })
77
+
78
+ app.get('/health', async (ctx) => ctx.text('ok'))
79
+
80
+ app.on<DeviceReading>('device.reading', { schema: 'events.DeviceReading' }, async (ctx) => {
81
+ const reading = ctx.event.payload
82
+ ctx.log.info('device reading accepted')
83
+ await ctx.emit('device.reading.accepted', {
84
+ schema: 'events.DeviceReadingAccepted',
85
+ payload: { deviceId: reading.deviceId, accepted: true },
86
+ })
87
+ })
88
+
89
+ app.on('system.tick', { schema: null }, async (ctx) => {
90
+ ctx.log.info('system tick accepted')
91
+ await ctx.emit('system.heartbeat', { schema: null })
92
+ })
93
+
94
+ export default app
95
+ ```
96
+ <!-- /pulse-doc-source -->
97
+
98
+ ## Schemas
99
+
100
+ <!-- pulse-doc-source: examples/11-events/src/schemas.ts -->
101
+ ```ts
102
+ import { defineSchemaRegistry, schema } from '@pulse-compute/pulse/schema'
103
+
104
+ export interface DeviceReading {
105
+ deviceId: string
106
+ temperatureC: number
107
+ }
108
+
109
+ export interface DeviceReadingAccepted {
110
+ deviceId: string
111
+ accepted: boolean
112
+ }
113
+
114
+ export default defineSchemaRegistry({
115
+ schemas: {
116
+ 'events.DeviceReading': schema<DeviceReading>(),
117
+ 'events.DeviceReadingAccepted': schema<DeviceReadingAccepted>(),
118
+ },
119
+ })
120
+ ```
121
+ <!-- /pulse-doc-source -->
122
+
123
+ ## Project config
124
+
125
+ <!-- pulse-doc-source: examples/11-events/.pulse/config.ts -->
126
+ ```ts
127
+ import { defineConfig } from '@pulse-compute/pulse'
128
+
129
+ export default defineConfig((_scope) => ({
130
+ pulse: {
131
+ entry: 'src/index.ts',
132
+ schema: 'src/schemas.ts',
133
+ tests: 'tests/pulse.harness.ts',
134
+ defaultProfile: 'node-native',
135
+ strict: true,
136
+ },
137
+ 'node-native': {
138
+ host: 'node',
139
+ target: 'native',
140
+ outDir: 'dist-node-native',
141
+ dev: { host: '127.0.0.1', port: 8787, networkFetch: false },
142
+ },
143
+ 'node-javascript': {
144
+ host: 'node',
145
+ target: 'javascript',
146
+ outDir: 'dist-node-javascript',
147
+ dev: { host: '127.0.0.1', port: 8787, networkFetch: false },
148
+ },
149
+ }))
150
+ ```
151
+ <!-- /pulse-doc-source -->
152
+
153
+ ## Test harness
154
+
155
+ <!-- pulse-doc-source: examples/11-events/tests/pulse.harness.ts -->
156
+ ```ts
157
+ export default { cases: [
158
+ {
159
+ name: 'http health remains separate',
160
+ request: { method: 'GET', path: '/health' },
161
+ expect: { status: 200, text: 'ok' },
162
+ },
163
+ {
164
+ name: 'schema event emits an accepted frame',
165
+ kind: 'event',
166
+ event: {
167
+ type: 'device.reading',
168
+ schema: 'events.DeviceReading',
169
+ payload: { deviceId: 'sensor-7', temperatureC: 21 },
170
+ },
171
+ expect: {
172
+ status: 'completed',
173
+ emitted: [{
174
+ type: 'device.reading.accepted',
175
+ schema: 'events.DeviceReadingAccepted',
176
+ payload: { deviceId: 'sensor-7', accepted: true },
177
+ }],
178
+ },
179
+ },
180
+ {
181
+ name: 'no-payload event emits a no-payload frame',
182
+ kind: 'event',
183
+ event: { type: 'system.tick', schema: null },
184
+ expect: {
185
+ status: 'completed',
186
+ emitted: [{ type: 'system.heartbeat', schema: null }],
187
+ },
188
+ },
189
+ ] }
190
+ ```
191
+ <!-- /pulse-doc-source -->
192
+
193
+ See [Static events and outbound emission](../../docs/guides/events.md) for the
194
+ frame, queue, target, diagnostic, and Native-extension contracts.
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@pulse-examples/11-events",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "doctor": "pulse doctor",
8
+ "inspect": "pulse inspect",
9
+ "test": "pulse test",
10
+ "dev": "pulse dev",
11
+ "build": "pulse build"
12
+ },
13
+ "dependencies": {
14
+ "@pulse-compute/pulse": "1.0.0-beta.2"
15
+ },
16
+ "devDependencies": {
17
+ "@pulse-compute/cli": "1.0.0-beta.2",
18
+ "typescript": "5.9.3"
19
+ }
20
+ }
@@ -0,0 +1,27 @@
1
+ import { Pulse } from '@pulse-compute/pulse'
2
+
3
+ type DeviceReading = Readonly<{
4
+ deviceId: string
5
+ temperatureC: number
6
+ }>
7
+
8
+ const app = new Pulse({ auto: true })
9
+
10
+ app.get('/health', async (ctx) => ctx.text('ok'))
11
+
12
+ app.on<DeviceReading>('device.reading', { schema: 'events.DeviceReading' }, async (ctx) => {
13
+ const reading = ctx.event.payload
14
+ ctx.log.info('device reading accepted')
15
+ await ctx.emit('device.reading.accepted', {
16
+ schema: 'events.DeviceReadingAccepted',
17
+ payload: { deviceId: reading.deviceId, accepted: true },
18
+ })
19
+ })
20
+
21
+ app.on('system.tick', { schema: null }, async (ctx) => {
22
+ ctx.log.info('system tick accepted')
23
+ await ctx.emit('system.heartbeat', { schema: null })
24
+ })
25
+
26
+ export default app
27
+
@@ -0,0 +1,19 @@
1
+ import { defineSchemaRegistry, schema } from '@pulse-compute/pulse/schema'
2
+
3
+ export interface DeviceReading {
4
+ deviceId: string
5
+ temperatureC: number
6
+ }
7
+
8
+ export interface DeviceReadingAccepted {
9
+ deviceId: string
10
+ accepted: boolean
11
+ }
12
+
13
+ export default defineSchemaRegistry({
14
+ schemas: {
15
+ 'events.DeviceReading': schema<DeviceReading>(),
16
+ 'events.DeviceReadingAccepted': schema<DeviceReadingAccepted>(),
17
+ },
18
+ })
19
+
@@ -0,0 +1,34 @@
1
+ export default { cases: [
2
+ {
3
+ name: 'http health remains separate',
4
+ request: { method: 'GET', path: '/health' },
5
+ expect: { status: 200, text: 'ok' },
6
+ },
7
+ {
8
+ name: 'schema event emits an accepted frame',
9
+ kind: 'event',
10
+ event: {
11
+ type: 'device.reading',
12
+ schema: 'events.DeviceReading',
13
+ payload: { deviceId: 'sensor-7', temperatureC: 21 },
14
+ },
15
+ expect: {
16
+ status: 'completed',
17
+ emitted: [{
18
+ type: 'device.reading.accepted',
19
+ schema: 'events.DeviceReadingAccepted',
20
+ payload: { deviceId: 'sensor-7', accepted: true },
21
+ }],
22
+ },
23
+ },
24
+ {
25
+ name: 'no-payload event emits a no-payload frame',
26
+ kind: 'event',
27
+ event: { type: 'system.tick', schema: null },
28
+ expect: {
29
+ status: 'completed',
30
+ emitted: [{ type: 'system.heartbeat', schema: null }],
31
+ },
32
+ },
33
+ ] }
34
+
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noEmit": true
8
+ },
9
+ "include": [
10
+ "src/**/*.ts",
11
+ ".pulse/**/*.ts",
12
+ "tests/**/*.ts"
13
+ ]
14
+ }
15
+
@@ -0,0 +1,3 @@
1
+ *
2
+ !.gitignore
3
+ !config.ts
@@ -0,0 +1,25 @@
1
+ import { defineConfig } from '@pulse-compute/pulse'
2
+
3
+ export default defineConfig((_scope) => ({
4
+ pulse: {
5
+ entry: 'src/index.ts',
6
+ tests: 'tests/pulse.harness.ts',
7
+ defaultProfile: 'local',
8
+ strict: true,
9
+ },
10
+ local: {
11
+ host: 'node',
12
+ target: 'native',
13
+ outDir: 'dist',
14
+ dev: {
15
+ fetches: {
16
+ 'POST https://mcp.example.test/rpc': {
17
+ opaque: true,
18
+ status: 200,
19
+ headers: [['content-type', 'application/json']],
20
+ chunks: ['{"jsonrpc":"2.0","result":{},"id":1}'],
21
+ },
22
+ },
23
+ },
24
+ },
25
+ }))