@sigmx/hono 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Callum Bonnyman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @sigmx/hono
2
+
3
+ Hono middleware for [sigmx](https://github.com/wrux/sigmx). One `app.use(sigmx())` and every handler gets `c.var.sigmx`: the signals the request carries, and responses the client knows how to apply.
4
+
5
+ ```bash
6
+ npm install sigmx @sigmx/hono
7
+ ```
8
+
9
+ ```ts
10
+ import { Hono } from 'hono';
11
+ import { patchElements, patchSignals, sigmx } from '@sigmx/hono';
12
+ import { serveClient } from '@sigmx/hono/node';
13
+
14
+ const app = new Hono();
15
+ app.use(sigmx());
16
+ app.get('/sigmx.js', serveClient()); // the script-tag build from node_modules; skip it if you bundle
17
+
18
+ app.get('/api/towns', async (c) => {
19
+ const { q = '' } = await c.var.sigmx.signals();
20
+ return c.var.sigmx.events(patchElements(renderTowns(q)), patchSignals({ stale: false }));
21
+ });
22
+
23
+ app.get('/api/progress', (c) =>
24
+ c.var.sigmx.stream(async (s) => {
25
+ for (let step = 1; step <= 10 && !s.closed; step++) {
26
+ await s.sleep(150);
27
+ await s.patchSignals({ progress: step * 10 });
28
+ }
29
+ }),
30
+ );
31
+ ```
32
+
33
+ ## `c.var.sigmx`
34
+
35
+ | member | |
36
+ |---|---|
37
+ | `isRequest` | true when the sigmx client made the request, so a route can return a partial instead of a whole page |
38
+ | `signals()` | the signals: `sigmx` query parameter on GET and DELETE, JSON body otherwise, form fields for `contentType: 'form'` requests |
39
+ | `signals(schema)` | the same, validated by any [Standard Schema](https://standardschema.dev) validator (Zod, Valibot, ArkType…) and typed from it |
40
+ | `html(body, { selector, mode, useViewTransition, status })` | HTML the client morphs by id, or into `selector` with `mode`. Plain `c.html()` already works when you morph by id |
41
+ | `json(signals, { onlyIfMissing, status })` | JSON the client merges into its signals |
42
+ | `events(...events)` | several patches in one response, sent at once |
43
+ | `stream(fn)` | a long-lived stream; `fn` gets a `SigmxStream` with `patchElements`, `patchSignals`, `removeElements`, `removeSignals`, `executeScript`, `sleep`, `closed` and `onAbort`. Built on Hono's `streamSSE`, so it stops when the client disconnects |
44
+
45
+ Event builders (`patchElements`, `patchSignals`, `removeElements`, `removeSignals`, `executeScript`) and `formatEvent` are exported for use with `events()` or your own transport.
46
+
47
+ ## Validation failures
48
+
49
+ `signals(schema)` throws a `SignalsError` (a Hono `HTTPException`) with the issues. Hono answers it with a JSON body and a 422 (400 when the payload could not be parsed) and logs nothing. To answer differently:
50
+
51
+ ```ts
52
+ app.use(sigmx({ onError: (error, c) => c.text(error.message, 422) }));
53
+ ```
54
+
55
+ If you define your own `app.onError`, return `error.getResponse()` for `HTTPException`s as usual.
56
+
57
+ ## Serving the client
58
+
59
+ `serveClient()` from `@sigmx/hono/node` reads `sigmx.standalone.js` from the installed `sigmx` package and serves it with an ETag and `cache-control`. It needs a filesystem, so it is a separate entry for Node, Bun and Deno; on Workers, bundle the client with your app instead. Pass `{ file, maxAge }` to serve another file from sigmx's `dist` or change the cache lifetime.
60
+
61
+ ## JSX
62
+
63
+ Hono's JSX passes namespaced attributes through, and every helper accepts a JSX node wherever it takes markup:
64
+
65
+ ```tsx
66
+ const Counter = () => <button data-on:click="$count++">Clicked <b data-text="$count">0</b> times</button>;
67
+
68
+ app.get('/', (c) => c.html(<Counter />));
69
+ app.get('/api/rows', async (c) => c.var.sigmx.events(patchElements(<Rows q={String((await c.var.sigmx.signals()).q)} />)));
70
+ ```
71
+
72
+ JSX attribute names may contain colons but not dots, so a modifier with an argument such as `data-on:input__debounce.200ms` is spread from an object: `<input {...{ 'data-on:input__debounce.200ms': "@get('/api/towns')" }} />`.
73
+
74
+ ## Licence
75
+
76
+ MIT
@@ -0,0 +1,72 @@
1
+ import type { Context, MiddlewareHandler } from 'hono';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import { type SSEStreamingApi } from 'hono/streaming';
4
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
5
+ import { type EventOptions, type ExecuteScriptOptions, type Markup, type PatchElementsOptions, type PatchSignalsOptions, type SchemaIssue, type ServerEvent, type StandardSchema } from 'sigmx/server';
6
+ export { EVENT_PATCH_ELEMENTS, EVENT_PATCH_SIGNALS, type EventOptions, type ExecuteScriptOptions, executeScript, formatEvent, type Markup, type PatchElementsOptions, type PatchMode, type PatchSignalsOptions, patchElements, patchSignals, removeElements, removeSignals, type SchemaIssue, type SchemaResult, type ServerEvent, SIGNALS_KEY, SSE_HEADERS, type StandardSchema, } from 'sigmx/server';
7
+ /**
8
+ * Thrown by `signals()` when the payload cannot be parsed (400) or fails validation (422). It is an
9
+ * `HTTPException`, so Hono's default error handling answers with `getResponse()` and logs nothing.
10
+ */
11
+ export declare class SignalsError extends HTTPException {
12
+ readonly issues: ReadonlyArray<SchemaIssue>;
13
+ constructor(message: string, issues?: ReadonlyArray<SchemaIssue>, status?: 400 | 422);
14
+ getResponse(): Response;
15
+ response(): Response;
16
+ }
17
+ export interface SigmxOptions {
18
+ /**
19
+ * Turn a `SignalsError` thrown by `signals(schema)` into a response.
20
+ * Default: the error's own JSON body with a 400 or 422 status.
21
+ */
22
+ onError?: (error: SignalsError, c: Context) => Response | Promise<Response>;
23
+ }
24
+ export interface SigmxHtmlOptions extends PatchElementsOptions {
25
+ status?: ContentfulStatusCode;
26
+ }
27
+ export interface SigmxJsonOptions extends PatchSignalsOptions {
28
+ status?: ContentfulStatusCode;
29
+ }
30
+ /** A live event stream. Every write is awaited so backpressure and disconnects are respected. */
31
+ export declare class SigmxStream {
32
+ private readonly api;
33
+ constructor(api: SSEStreamingApi);
34
+ get closed(): boolean;
35
+ send(event: ServerEvent): Promise<void>;
36
+ patchElements(html: Markup, o?: PatchElementsOptions): Promise<void>;
37
+ removeElements(selector: string, o?: EventOptions): Promise<void>;
38
+ patchSignals(signals: Record<string, unknown> | string, o?: PatchSignalsOptions): Promise<void>;
39
+ removeSignals(paths: string[], o?: EventOptions): Promise<void>;
40
+ executeScript(script: string, o?: ExecuteScriptOptions): Promise<void>;
41
+ /** Wait, resolving early if the client disconnects. */
42
+ sleep(ms: number): Promise<unknown>;
43
+ onAbort(fn: () => void | Promise<void>): void;
44
+ close(): Promise<void>;
45
+ }
46
+ export interface SigmxContext {
47
+ /** True when the request was made by the sigmx client. Use it to send a partial instead of a whole page. */
48
+ readonly isRequest: boolean;
49
+ /**
50
+ * The signals the request carries: the `sigmx` query parameter on GET and DELETE, a JSON body otherwise,
51
+ * or form fields for `contentType: 'form'` requests. With a Standard Schema validator (Zod, Valibot, ArkType…)
52
+ * the result is typed and invalid payloads become a 422 response.
53
+ */
54
+ signals(): Promise<Record<string, any>>;
55
+ signals<T>(schema: StandardSchema<T>): Promise<T>;
56
+ /** HTML the client morphs by id, or into `selector` with `mode`. Accepts strings, `html` templates and Hono JSX. */
57
+ html(body: string | Promise<string>, o?: SigmxHtmlOptions): Response | Promise<Response>;
58
+ json(signals: Record<string, unknown>, o?: SigmxJsonOptions): Response;
59
+ events(...events: ServerEvent[]): Response;
60
+ /** A long-lived stream; the response closes when `fn` resolves or the client disconnects. */
61
+ stream(fn: (stream: SigmxStream) => Promise<void> | void): Response;
62
+ }
63
+ declare module 'hono' {
64
+ interface ContextVariableMap {
65
+ sigmx: SigmxContext;
66
+ }
67
+ }
68
+ /**
69
+ * Middleware that attaches `c.var.sigmx` to every request. A `SignalsError` escaping a handler is turned
70
+ * into its JSON response (or whatever `onError` returns); every other error is left to `app.onError`.
71
+ */
72
+ export declare const sigmx: (o?: SigmxOptions) => MiddlewareHandler;
package/dist/index.js ADDED
@@ -0,0 +1,126 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import { streamSSE } from 'hono/streaming';
4
+ import { executeScript, formatEvent, patchElements, patchSignals, removeElements, removeSignals, SIGNALS_KEY, SSE_HEADERS, validateSignals, } from 'sigmx/server';
5
+ export { EVENT_PATCH_ELEMENTS, EVENT_PATCH_SIGNALS, executeScript, formatEvent, patchElements, patchSignals, removeElements, removeSignals, SIGNALS_KEY, SSE_HEADERS, } from 'sigmx/server';
6
+ /**
7
+ * Thrown by `signals()` when the payload cannot be parsed (400) or fails validation (422). It is an
8
+ * `HTTPException`, so Hono's default error handling answers with `getResponse()` and logs nothing.
9
+ */
10
+ export class SignalsError extends HTTPException {
11
+ issues;
12
+ constructor(message, issues = [], status = 422) {
13
+ super(status, { message });
14
+ this.name = 'SignalsError';
15
+ this.issues = issues;
16
+ }
17
+ getResponse() {
18
+ return Response.json({ error: this.message, issues: this.issues }, { status: this.status });
19
+ }
20
+ response() {
21
+ return this.getResponse();
22
+ }
23
+ }
24
+ /** A live event stream. Every write is awaited so backpressure and disconnects are respected. */
25
+ export class SigmxStream {
26
+ api;
27
+ constructor(api) {
28
+ this.api = api;
29
+ }
30
+ get closed() {
31
+ return this.api.closed || this.api.aborted;
32
+ }
33
+ send(event) {
34
+ if (this.closed)
35
+ return Promise.resolve();
36
+ return this.api.writeSSE({ event: event.event, data: event.lines.join('\n'), id: event.id, retry: event.retry });
37
+ }
38
+ patchElements(html, o) {
39
+ return this.send(patchElements(html, o));
40
+ }
41
+ removeElements(selector, o) {
42
+ return this.send(removeElements(selector, o));
43
+ }
44
+ patchSignals(signals, o) {
45
+ return this.send(patchSignals(signals, o));
46
+ }
47
+ removeSignals(paths, o) {
48
+ return this.send(removeSignals(paths, o));
49
+ }
50
+ executeScript(script, o) {
51
+ return this.send(executeScript(script, o));
52
+ }
53
+ /** Wait, resolving early if the client disconnects. */
54
+ sleep(ms) {
55
+ return this.api.sleep(ms);
56
+ }
57
+ onAbort(fn) {
58
+ this.api.onAbort(fn);
59
+ }
60
+ close() {
61
+ return this.api.close();
62
+ }
63
+ }
64
+ const readSignals = async (c, schema) => {
65
+ let raw;
66
+ const method = c.req.method.toUpperCase();
67
+ const type = c.req.header('content-type') ?? '';
68
+ try {
69
+ if (method === 'GET' || method === 'DELETE') {
70
+ const q = c.req.query(SIGNALS_KEY);
71
+ raw = q ? JSON.parse(q) : {};
72
+ }
73
+ else if (type.includes('application/json')) {
74
+ const text = await c.req.text();
75
+ raw = text ? JSON.parse(text) : {};
76
+ }
77
+ else if (type.includes('form')) {
78
+ raw = await c.req.parseBody();
79
+ }
80
+ else {
81
+ raw = {};
82
+ }
83
+ }
84
+ catch (e) {
85
+ throw new SignalsError(`could not parse signals: ${e.message}`, [], 400);
86
+ }
87
+ try {
88
+ return await validateSignals(raw, schema);
89
+ }
90
+ catch (e) {
91
+ const err = e;
92
+ throw new SignalsError(err.message, err.issues ?? [], 422);
93
+ }
94
+ };
95
+ const contextFor = (c) => ({
96
+ isRequest: c.req.header('sigmx-request') === 'true',
97
+ signals: (schema) => readSignals(c, schema),
98
+ html: (body, o = {}) => {
99
+ if (o.selector)
100
+ c.header('sigmx-selector', o.selector);
101
+ if (o.mode)
102
+ c.header('sigmx-mode', o.mode);
103
+ if (o.useViewTransition)
104
+ c.header('sigmx-use-view-transition', 'true');
105
+ return c.html(body, o.status ?? 200);
106
+ },
107
+ json: (signals, o = {}) => {
108
+ if (o.onlyIfMissing)
109
+ c.header('sigmx-only-if-missing', 'true');
110
+ return c.json(signals, o.status ?? 200);
111
+ },
112
+ events: (...events) => c.body(events.map(formatEvent).join(''), 200, SSE_HEADERS),
113
+ stream: (fn) => streamSSE(c, async (api) => {
114
+ await fn(new SigmxStream(api));
115
+ }),
116
+ });
117
+ /**
118
+ * Middleware that attaches `c.var.sigmx` to every request. A `SignalsError` escaping a handler is turned
119
+ * into its JSON response (or whatever `onError` returns); every other error is left to `app.onError`.
120
+ */
121
+ export const sigmx = (o = {}) => createMiddleware(async (c, next) => {
122
+ c.set('sigmx', contextFor(c));
123
+ await next();
124
+ if (c.error instanceof SignalsError)
125
+ c.res = o.onError ? await o.onError(c.error, c) : c.error.response();
126
+ });
package/dist/node.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Handler } from 'hono';
2
+ export interface ServeClientOptions {
3
+ /** File inside sigmx's `dist` to serve (default `sigmx.standalone.js`, every plugin, minified). */
4
+ file?: string;
5
+ /** `max-age` in seconds for the `cache-control` header (default one hour); an ETag handles the rest. */
6
+ maxAge?: number;
7
+ }
8
+ /**
9
+ * A route handler that serves the client from `node_modules/sigmx` with an ETag, so no bundler is needed:
10
+ *
11
+ * app.get('/sigmx.js', serveClient())
12
+ */
13
+ export declare const serveClient: (o?: ServeClientOptions) => Handler;
package/dist/node.js ADDED
@@ -0,0 +1,32 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join } from 'node:path';
5
+ /**
6
+ * A route handler that serves the client from `node_modules/sigmx` with an ETag, so no bundler is needed:
7
+ *
8
+ * app.get('/sigmx.js', serveClient())
9
+ */
10
+ export const serveClient = (o = {}) => {
11
+ let cached;
12
+ const load = () => {
13
+ cached ??= (async () => {
14
+ const require = createRequire(import.meta.url);
15
+ const dist = join(dirname(require.resolve('sigmx/package.json')), 'dist');
16
+ const body = await readFile(join(dist, o.file ?? 'sigmx.standalone.js'), 'utf8');
17
+ const etag = `"${createHash('sha1').update(body).digest('base64url').slice(0, 16)}"`;
18
+ return { body, etag };
19
+ })();
20
+ return cached;
21
+ };
22
+ return async (c) => {
23
+ const { body, etag } = await load();
24
+ if (c.req.header('if-none-match') === etag)
25
+ return c.body(null, 304);
26
+ return c.body(body, 200, {
27
+ 'content-type': 'text/javascript; charset=utf-8',
28
+ etag,
29
+ 'cache-control': `public, max-age=${o.maxAge ?? 3600}`,
30
+ });
31
+ };
32
+ };
package/dist/wire.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ /** Query-string key that carries signals on GET/DELETE requests. */
2
+ export declare const SIGNALS_KEY = "sigmx";
3
+ export declare const EVENT_PATCH_ELEMENTS = "sigmx-patch-elements";
4
+ export declare const EVENT_PATCH_SIGNALS = "sigmx-patch-signals";
5
+ export declare const SSE_HEADERS: {
6
+ readonly 'content-type': "text/event-stream";
7
+ readonly 'cache-control': "no-cache";
8
+ readonly connection: "keep-alive";
9
+ };
10
+ /** HTML as a string, or anything that renders to one such as a Hono JSX node or an `html` template. */
11
+ export type Markup = string | {
12
+ toString(): string;
13
+ };
14
+ export type PatchMode = 'outer' | 'inner' | 'replace' | 'prepend' | 'append' | 'before' | 'after' | 'remove';
15
+ export interface ServerEvent {
16
+ event: string;
17
+ /** Data lines, each `key value`. */
18
+ lines: string[];
19
+ id?: string;
20
+ retry?: number;
21
+ }
22
+ export interface EventOptions {
23
+ id?: string;
24
+ retry?: number;
25
+ }
26
+ export interface PatchElementsOptions extends EventOptions {
27
+ selector?: string;
28
+ mode?: PatchMode;
29
+ useViewTransition?: boolean;
30
+ }
31
+ export interface PatchSignalsOptions extends EventOptions {
32
+ onlyIfMissing?: boolean;
33
+ }
34
+ export interface ExecuteScriptOptions extends EventOptions {
35
+ /** Remove the script element after it runs (default true). */
36
+ autoRemove?: boolean;
37
+ attributes?: Record<string, string>;
38
+ }
39
+ /** Patch HTML into the page. Without a selector, top-level elements are morphed by id. */
40
+ export declare const patchElements: (html: Markup, o?: PatchElementsOptions) => ServerEvent;
41
+ export declare const removeElements: (selector: string, o?: EventOptions) => ServerEvent;
42
+ /** Merge signals (JSON merge-patch: null removes). Accepts an object or pre-serialised JSON. */
43
+ export declare const patchSignals: (signals: Record<string, unknown> | string, o?: PatchSignalsOptions) => ServerEvent;
44
+ /** Remove signals by dotted path. */
45
+ export declare const removeSignals: (paths: string[], o?: EventOptions) => ServerEvent;
46
+ /** Run JavaScript in the page by appending a script element to the body. */
47
+ export declare const executeScript: (script: string, o?: ExecuteScriptOptions) => ServerEvent;
48
+ /** Serialise one event in `text/event-stream` format. */
49
+ export declare const formatEvent: (e: ServerEvent) => string;
50
+ /** Minimal Standard Schema (standardschema.dev) so any compliant validator works without a dependency. */
51
+ export interface StandardSchema<Output = unknown> {
52
+ readonly '~standard': {
53
+ readonly validate: (value: unknown) => SchemaResult<Output> | Promise<SchemaResult<Output>>;
54
+ };
55
+ }
56
+ export type SchemaIssue = {
57
+ readonly message: string;
58
+ readonly path?: ReadonlyArray<PropertyKey | {
59
+ readonly key: PropertyKey;
60
+ }>;
61
+ };
62
+ export type SchemaResult<T> = {
63
+ readonly value: T;
64
+ readonly issues?: undefined;
65
+ } | {
66
+ readonly issues: ReadonlyArray<SchemaIssue>;
67
+ };
package/dist/wire.js ADDED
@@ -0,0 +1,73 @@
1
+ // The sigmx wire format: event builders and the signal-reading contract. Framework-neutral.
2
+ /** Query-string key that carries signals on GET/DELETE requests. */
3
+ export const SIGNALS_KEY = 'sigmx';
4
+ export const EVENT_PATCH_ELEMENTS = 'sigmx-patch-elements';
5
+ export const EVENT_PATCH_SIGNALS = 'sigmx-patch-signals';
6
+ export const SSE_HEADERS = {
7
+ 'content-type': 'text/event-stream',
8
+ 'cache-control': 'no-cache',
9
+ connection: 'keep-alive',
10
+ };
11
+ /** Patch HTML into the page. Without a selector, top-level elements are morphed by id. */
12
+ export const patchElements = (html, o = {}) => {
13
+ const lines = [];
14
+ if (o.selector)
15
+ lines.push(`selector ${o.selector}`);
16
+ if (o.mode && o.mode !== 'outer')
17
+ lines.push(`mode ${o.mode}`);
18
+ if (o.useViewTransition)
19
+ lines.push('useViewTransition true');
20
+ for (const l of String(html).trim().split('\n'))
21
+ lines.push(`elements ${l}`);
22
+ return { event: EVENT_PATCH_ELEMENTS, lines, id: o.id, retry: o.retry };
23
+ };
24
+ export const removeElements = (selector, o = {}) => ({
25
+ event: EVENT_PATCH_ELEMENTS,
26
+ lines: [`selector ${selector}`, 'mode remove'],
27
+ ...o,
28
+ });
29
+ /** Merge signals (JSON merge-patch: null removes). Accepts an object or pre-serialised JSON. */
30
+ export const patchSignals = (signals, o = {}) => ({
31
+ event: EVENT_PATCH_SIGNALS,
32
+ lines: [
33
+ ...(o.onlyIfMissing ? ['onlyIfMissing true'] : []),
34
+ `signals ${typeof signals === 'string' ? signals : JSON.stringify(signals)}`,
35
+ ],
36
+ id: o.id,
37
+ retry: o.retry,
38
+ });
39
+ /** Remove signals by dotted path. */
40
+ export const removeSignals = (paths, o = {}) => {
41
+ const patch = {};
42
+ for (const p of paths) {
43
+ const keys = p.split('.');
44
+ const last = keys.pop();
45
+ let cur = patch;
46
+ for (const k of keys)
47
+ cur = cur[k] ??= {};
48
+ cur[last] = null;
49
+ }
50
+ return patchSignals(patch, o);
51
+ };
52
+ /** Run JavaScript in the page by appending a script element to the body. */
53
+ export const executeScript = (script, o = {}) => {
54
+ const attrs = { ...((o.autoRemove ?? true) ? { 'data-init': 'el.remove()' } : {}), ...o.attributes };
55
+ const attrText = Object.entries(attrs)
56
+ .map(([k, v]) => ` ${k}="${v.replace(/"/g, '&quot;')}"`)
57
+ .join('');
58
+ return patchElements(`<script${attrText}>${script}</script>`, {
59
+ selector: 'body',
60
+ mode: 'append',
61
+ id: o.id,
62
+ retry: o.retry,
63
+ });
64
+ };
65
+ /** Serialise one event in `text/event-stream` format. */
66
+ export const formatEvent = (e) => `${[
67
+ e.id !== undefined && `id: ${e.id}`,
68
+ e.retry !== undefined && `retry: ${e.retry}`,
69
+ `event: ${e.event}`,
70
+ ...e.lines.map((l) => `data: ${l}`),
71
+ ]
72
+ .filter(Boolean)
73
+ .join('\n')}\n\n`;
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@sigmx/hono",
3
+ "version": "0.1.0",
4
+ "description": "Hono middleware for sigmx: read signals from the request, answer with HTML, JSON or a stream of patches, serve the client.",
5
+ "keywords": [
6
+ "hono",
7
+ "hono-middleware",
8
+ "sigmx",
9
+ "hypermedia",
10
+ "sse"
11
+ ],
12
+ "author": "Callum Bonnyman",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/wrux/sigmx.git",
17
+ "directory": "sdks/hono"
18
+ },
19
+ "homepage": "https://github.com/wrux/sigmx#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/wrux/sigmx/issues"
22
+ },
23
+ "type": "module",
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./node": {
35
+ "types": "./dist/node.d.ts",
36
+ "default": "./dist/node.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.json",
48
+ "test": "npm run build && node --test tests/*.test.mjs",
49
+ "prepublishOnly": "npm test",
50
+ "prepack": "npm run build"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.20.1",
54
+ "hono": "^4.13.7",
55
+ "sigmx": "file:../..",
56
+ "typescript": "^5.9.0"
57
+ },
58
+ "peerDependencies": {
59
+ "hono": ">=4.6.0",
60
+ "sigmx": "^0.1.0"
61
+ }
62
+ }