silgi 0.52.2 → 0.53.1

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.
@@ -2,17 +2,9 @@
2
2
  /**
3
3
  * Build a {@link SchemaRegistry} from an array of converters.
4
4
  *
5
- * @param converters - Array of {@link SchemaConverter} objects, each
6
- * declaring their own `vendor`.
7
- * @returns A `Map<string, SchemaConverter>` keyed by `converter.vendor`.
8
- *
9
5
  * @example
10
- * ```ts
11
- * import { zodConverter } from 'silgi/zod'
12
- * import { createSchemaRegistry } from 'silgi'
13
- *
14
- * const registry = createSchemaRegistry([zodConverter])
15
- * ```
6
+ * import { zodConverter } from 'silgi/zod'
7
+ * const registry = createSchemaRegistry([zodConverter])
16
8
  *
17
9
  * @category Schema
18
10
  */
@@ -21,62 +13,99 @@ function createSchemaRegistry(converters = []) {
21
13
  for (const converter of converters) map.set(converter.vendor, converter);
22
14
  return map;
23
15
  }
24
- const _warnedVendors = /* @__PURE__ */ new Set();
25
16
  /**
26
- * Convert any Standard Schema to JSON Schema.
17
+ * Vendor names we've already warned about. Module-scoped so the
18
+ * warning fires at most once per vendor per process lifetime — across
19
+ * every silgi instance in the same process. The set only holds bounded
20
+ * vendor strings (no schema data, no user input), so sharing it
21
+ * globally is safe.
22
+ */
23
+ const warnedVendors = /* @__PURE__ */ new Set();
24
+ /** Read the `~standard` slot off an AnySchema in one typed step. */
25
+ const readStandardSlot = (schema) => schema["~standard"];
26
+ /**
27
+ * Call the Standard JSON Schema extension on a schema that exposes it.
28
+ * Returns `null` when the extension does not handle this strategy or
29
+ * throws (in which case the caller falls back to the registry).
27
30
  *
28
- * @remarks
29
- * Resolution order:
30
- * 1. **Native fast path** — `schema['~standard'].jsonSchema.input()`
31
- * (Valibot, ArkType, Zod v4, …). No registry needed.
32
- * 2. **Registry lookup** finds a converter by
33
- * `schema['~standard'].vendor`. Registry must be passed explicitly;
34
- * there is no global mutable state.
35
- * 3. **Empty schema `{}`** — emits a one-time `console.warn` per vendor
36
- * when a registry was provided but contained no matching converter.
37
- * No warn when no registry was passed (caller opted out).
31
+ * We strip the `$schema` meta field because it is a JSON Schema marker,
32
+ * not a member of the schema itself — silgi never propagates it.
33
+ */
34
+ function callNativeJsonSchema(std, opts) {
35
+ const generator = opts.strategy === "output" ? std.jsonSchema?.output : std.jsonSchema?.input;
36
+ if (!generator) return null;
37
+ try {
38
+ const result = generator({
39
+ target: opts.target,
40
+ libraryOptions: opts.libraryOptions
41
+ });
42
+ if (!result || typeof result !== "object") return null;
43
+ const { $schema: _ignored, ...rest } = result;
44
+ return rest;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ /** Warn once per vendor when a registry was passed but had no match. */
50
+ function warnMissingConverter(vendor) {
51
+ if (warnedVendors.has(vendor)) return;
52
+ warnedVendors.add(vendor);
53
+ console.warn(`[silgi] No schema converter registered for vendor "${vendor}". Pass schemaConverters: [${vendor}Converter] to silgi() to enable OpenAPI / analytics schema generation.`);
54
+ }
55
+ /**
56
+ * Convert any Standard Schema to a JSON Schema object.
38
57
  *
39
- * @param schema - Any Standard Schema compatible schema object.
40
- * @param strategy - `'input'` (default) for pre-transform types; `'output'`
41
- * for post-transform.
42
- * @param registry - Optional {@link SchemaRegistry} built from
43
- * {@link createSchemaRegistry}. When omitted the function still handles
44
- * schemas that expose the native `jsonSchema.input()` fast path.
45
- * @returns A JSON Schema object. Returns `{}` when conversion is not possible.
58
+ * @param schema The schema to convert.
59
+ * @param strategy `'input'` (default) for pre-transform types, `'output'`
60
+ * for post-transform. Matters for schemas that coerce
61
+ * (e.g. `z.coerce.number()` takes a string and yields a
62
+ * number input and output schemas differ).
63
+ * @param registry Optional fallback registry built by
64
+ * {@link createSchemaRegistry}. Omit to rely solely on
65
+ * the native Standard JSON Schema extension.
66
+ * @param options Extra knobs: `target` dialect (default
67
+ * `'draft-2020-12'`), opaque `libraryOptions`
68
+ * forwarded to the underlying library.
69
+ *
70
+ * @returns A JSON Schema object. `{}` when the schema cannot be
71
+ * converted (silent fallback — analytics / OpenAPI output
72
+ * still renders, just without schema detail for that field).
46
73
  *
47
74
  * @example
48
- * ```ts
49
- * import { zodConverter } from 'silgi/zod'
50
- * import { createSchemaRegistry, schemaToJsonSchema } from 'silgi'
51
- * import { z } from 'zod'
75
+ * import { zodConverter } from 'silgi/zod'
76
+ * import { createSchemaRegistry, schemaToJsonSchema } from 'silgi'
52
77
  *
53
- * const registry = createSchemaRegistry([zodConverter])
54
- * const json = schemaToJsonSchema(z.object({ name: z.string() }), 'input', registry)
55
- * ```
78
+ * const registry = createSchemaRegistry([zodConverter])
79
+ * const json = schemaToJsonSchema(MySchema, 'input', registry)
56
80
  *
57
81
  * @category Schema
58
82
  */
59
- function schemaToJsonSchema(schema, strategy = "input", registry) {
60
- const std = schema?.["~standard"];
61
- if (std?.jsonSchema?.input) try {
62
- const result = std.jsonSchema.input({ target: "draft-2020-12" });
63
- if (result && typeof result === "object") {
64
- const { $schema: _, ...rest } = result;
65
- return rest;
66
- }
67
- } catch {}
68
- const vendor = typeof std?.vendor === "string" ? std.vendor : void 0;
69
- if (vendor && registry) {
70
- const converter = registry.get(vendor);
71
- if (converter) try {
72
- return converter.toJsonSchema(schema, { strategy });
73
- } catch {}
74
- else if (!_warnedVendors.has(vendor)) {
75
- _warnedVendors.add(vendor);
76
- console.warn(`[silgi] No schema converter registered for vendor "${vendor}". Pass schemaConverters: [${vendor}Converter] to silgi() to enable OpenAPI / analytics schema generation.`);
77
- }
83
+ function schemaToJsonSchema(schema, strategy = "input", registry, options = {}) {
84
+ const std = readStandardSlot(schema);
85
+ if (!std) return {};
86
+ const target = options.target ?? "draft-2020-12";
87
+ const native = callNativeJsonSchema(std, {
88
+ strategy,
89
+ target,
90
+ libraryOptions: options.libraryOptions
91
+ });
92
+ if (native !== null) return native;
93
+ const vendor = typeof std.vendor === "string" ? std.vendor : void 0;
94
+ if (!vendor || !registry) return {};
95
+ const converter = registry.get(vendor);
96
+ if (!converter) {
97
+ warnMissingConverter(vendor);
98
+ return {};
99
+ }
100
+ try {
101
+ return converter.toJsonSchema(schema, {
102
+ strategy,
103
+ target,
104
+ libraryOptions: options.libraryOptions
105
+ });
106
+ } catch {
107
+ return {};
78
108
  }
79
- return {};
80
109
  }
81
110
  //#endregion
82
111
  export { createSchemaRegistry, schemaToJsonSchema };
@@ -5,52 +5,53 @@ import { Hookable } from "hookable";
5
5
 
6
6
  //#region src/core/serve.d.ts
7
7
  interface SilgiServer {
8
- /** Server URL (e.g. "http://127.0.0.1:3000") */
8
+ /** Full server URL, e.g. `http://127.0.0.1:3000`. */
9
9
  readonly url: string;
10
- /** Configured port */
10
+ /** Port the server actually bound to (may differ from requested when `0`). */
11
11
  readonly port: number;
12
- /** Configured hostname */
12
+ /** Hostname the server bound to. */
13
13
  readonly hostname: string;
14
14
  /**
15
- * Gracefully shut down the server.
15
+ * Gracefully shut the server down.
16
16
  *
17
- * By default waits for in-flight requests to complete.
18
- * Pass `true` to forcefully terminate all active connections immediately.
17
+ * Waits for in-flight requests by default. Pass `true` to drop
18
+ * active connections immediately.
19
19
  */
20
20
  close(forceCloseConnections?: boolean): Promise<void>;
21
21
  }
22
22
  interface ServeOptions {
23
- /** URL path prefix (e.g. "/api"). Only requests matching this prefix are handled; others return 404. */
23
+ /** URL path prefix (e.g. `/api`). Requests outside the prefix 404. */
24
24
  basePath?: string;
25
25
  port?: number;
26
26
  hostname?: string;
27
- /** Enable Scalar API Reference UI at /api/reference and /api/openapi.json */
27
+ /** Mount the Scalar API Reference UI at `/api/reference`. */
28
28
  scalar?: boolean | ScalarOptions;
29
- /** Enable analytics dashboard at /api/analytics requires `auth` to be set */
29
+ /** Mount the analytics dashboard at `/api/analytics` (requires `auth`). */
30
30
  analytics?: AnalyticsOptions;
31
31
  /**
32
32
  * WebSocket RPC configuration.
33
33
  *
34
- * Defaults to auto-enabled when the router contains any subscription procedure.
35
- * Pass `false` to disable, or an options object to fine-tune crossws (compression, keepalive, maxPayload).
34
+ * Auto-enabled when the router contains any subscription procedure.
35
+ * Pass `false` to disable, or an options object to tune crossws
36
+ * (compression, keepalive, maxPayload).
36
37
  */
37
38
  ws?: false | WSAdapterOptions;
38
- /** Enable HTTP/2 (requires cert + key for TLS) */
39
+ /** TLS material for HTTP/2. When set, the server serves HTTPS. */
39
40
  http2?: {
40
41
  cert: string;
41
42
  key: string;
42
43
  };
43
44
  /**
44
- * Graceful shutdown on SIGINT/SIGTERM signals.
45
+ * Graceful shutdown on `SIGINT` / `SIGTERM`.
45
46
  *
46
- * - `true` (default): enables graceful shutdown with srvx defaults
47
- * - `false`: disables automatic signal handling
48
- * - `object`: fine-tune timeouts
47
+ * - `true` enable with srvx defaults (recommended).
48
+ * - `false` — disable automatic signal handling.
49
+ * - object fine-tune timeouts.
49
50
  *
50
51
  * @default true
51
52
  */
52
53
  gracefulShutdown?: boolean | {
53
- /** Max time (ms) to wait for in-flight requests before force-closing */timeout?: number; /** Max time (ms) after graceful period before process.exit */
54
+ /** Max ms to wait for in-flight requests before force-closing. */timeout?: number; /** Max ms after graceful period before `process.exit`. */
54
55
  forceTimeout?: number;
55
56
  };
56
57
  }
@@ -3,19 +3,156 @@ import { createFetchHandler, wrapHandler } from "./handler.mjs";
3
3
  import { _createWSHooks } from "../ws.mjs";
4
4
  import { serve } from "srvx";
5
5
  //#region src/core/serve.ts
6
+ /**
7
+ * `serve()` orchestrator
8
+ * ------------------------
9
+ *
10
+ * Builds a Node/Bun/Deno HTTP server from a silgi router. The heavy
11
+ * lifting (the compiled Fetch handler, analytics/Scalar wrappers) is
12
+ * already done elsewhere; this module stitches them together with the
13
+ * runtime-specific WebSocket upgrade plumbing and hands off to `srvx`.
14
+ *
15
+ * The only per-runtime work that lives here is mounting WebSocket hooks
16
+ * for subscriptions:
17
+ *
18
+ * - **Bun** — inject crossws into `serve({ bun: { websocket } })`
19
+ * and intercept upgrade requests at the Fetch layer.
20
+ * - **Deno** — intercept upgrade requests at the Fetch layer and
21
+ * call the crossws Deno adapter directly.
22
+ * - **Node** — after srvx exposes the `http.Server`, attach crossws
23
+ * via `server.on('upgrade', …)`.
24
+ *
25
+ * Everything else is shared: URL resolution, graceful shutdown, hook
26
+ * firing, and the startup banner.
27
+ */
28
+ /**
29
+ * Detect the current JavaScript runtime from well-known globals.
30
+ *
31
+ * Written as a plain function rather than reading a module-global so
32
+ * the check re-evaluates when the module is imported into a different
33
+ * runtime (e.g. a test that spawns a Bun child process).
34
+ */
6
35
  function detectRuntime() {
7
36
  if (typeof globalThis.Bun !== "undefined") return "bun";
8
37
  if (typeof globalThis.Deno !== "undefined") return "deno";
9
38
  return "node";
10
39
  }
40
+ /**
41
+ * Walk the router tree looking for a subscription. We stop at the first
42
+ * hit because we only need to decide whether to wire up WS at all — we
43
+ * do not need an inventory.
44
+ *
45
+ * Mirrors the helper in `silgi.ts`; kept here so `core/serve.ts` has no
46
+ * runtime dependency on the top-level instance module.
47
+ */
11
48
  function routerHasSubscription(def) {
12
49
  if (!def || typeof def !== "object") return false;
13
50
  if (def.type === "subscription") return true;
14
- for (const v of Object.values(def)) if (routerHasSubscription(v)) return true;
51
+ for (const child of Object.values(def)) if (routerHasSubscription(child)) return true;
15
52
  return false;
16
53
  }
54
+ /**
55
+ * Translate our `gracefulShutdown` option into the shape srvx expects.
56
+ *
57
+ * srvx uses `gracefulTimeout`, we expose `timeout` — the rename keeps
58
+ * the public API readable without leaking srvx vocabulary.
59
+ */
60
+ function resolveShutdown(option) {
61
+ if (option === void 0 || typeof option === "boolean") return option ?? true;
62
+ return {
63
+ gracefulTimeout: option.timeout,
64
+ forceTimeout: option.forceTimeout
65
+ };
66
+ }
67
+ /**
68
+ * Build the WS wiring for the current runtime.
69
+ *
70
+ * Everything stays lazy: when no subscriptions exist or WS is
71
+ * explicitly disabled, we return `{ fetch: httpHandler }` and never
72
+ * import the crossws adapters.
73
+ */
74
+ async function wireWebSocket(routerDef, httpHandler, enabled, wsOpts, runtime, bunServerRef) {
75
+ if (!enabled) return { fetch: httpHandler };
76
+ const hooksObj = _createWSHooks(routerDef, wsOpts);
77
+ if (runtime === "bun") {
78
+ const bunAdapter = (await import("crossws/adapters/bun")).default;
79
+ const adapter = bunAdapter({ hooks: hooksObj });
80
+ return {
81
+ bunWebsocket: adapter.websocket,
82
+ fetch: (async (req) => {
83
+ if (req.headers.get("upgrade") === "websocket" && bunServerRef.current) {
84
+ const res = await adapter.handleUpgrade(req, bunServerRef.current);
85
+ if (res) return res;
86
+ }
87
+ return httpHandler(req);
88
+ })
89
+ };
90
+ }
91
+ if (runtime === "deno") {
92
+ const denoAdapter = (await import("crossws/adapters/deno")).default;
93
+ const adapter = denoAdapter({ hooks: hooksObj });
94
+ return { fetch: (async (req) => {
95
+ if (req.headers.get("upgrade") === "websocket") return adapter.handleUpgrade(req, {});
96
+ return httpHandler(req);
97
+ }) };
98
+ }
99
+ return {
100
+ fetch: httpHandler,
101
+ attachNode: async (httpServer) => {
102
+ const { attachWebSocket } = await import("../ws.mjs");
103
+ await attachWebSocket(httpServer, routerDef, wsOpts);
104
+ }
105
+ };
106
+ }
107
+ /**
108
+ * Compute the final server URL from srvx's output.
109
+ *
110
+ * srvx usually populates `server.url` itself, but when the caller
111
+ * requests port `0` (pick-any-free) or when HTTP/2 is on, we have to
112
+ * piece it together from the runtime-specific socket info. Trailing
113
+ * slashes are stripped so `${url}/api/foo` always produces exactly one
114
+ * separator.
115
+ */
116
+ function resolveUrl(server, requestedPort, hostname, http2) {
117
+ let port = requestedPort;
118
+ if (server.node?.server) {
119
+ const addr = server.node.server.address();
120
+ if (addr && typeof addr === "object") port = addr.port;
121
+ } else if (server.bun?.server) port = server.bun.server.port ?? requestedPort;
122
+ const protocol = http2 ? "https" : "http";
123
+ const raw = server.url || `${protocol}://${hostname}:${port}`;
124
+ return {
125
+ url: raw.endsWith("/") ? raw.slice(0, -1) : raw,
126
+ port
127
+ };
128
+ }
129
+ /**
130
+ * Print the startup banner.
131
+ *
132
+ * Side-effect-y and intentionally not bypassable — a server starting
133
+ * silently is a surprising default; `silent: true` on srvx suppresses
134
+ * *its* banner, but silgi still wants to show where it bound.
135
+ */
136
+ function printBanner(url, hostname, port, runtime, options, wsEnabled) {
137
+ console.log(`\nSilgi server running at ${url}`);
138
+ if (options?.http2) console.log(` HTTP/2 enabled (with HTTP/1.1 fallback)`);
139
+ if (wsEnabled) console.log(` WebSocket RPC at ws://${hostname}:${port}/_ws (${runtime})`);
140
+ if (options?.scalar) console.log(` Scalar API Reference at ${url}/api/reference`);
141
+ if (options?.analytics) console.log(` Analytics dashboard at ${url}/api/analytics`);
142
+ console.log();
143
+ }
144
+ /**
145
+ * Build and start the HTTP (and optionally WebSocket) server.
146
+ *
147
+ * The function is intentionally long because the steps are strictly
148
+ * ordered: WS wiring has to happen before srvx starts (Bun needs its
149
+ * websocket handler at construction time); the `http.Server` only
150
+ * exists once srvx returns (Node attaches WS there); and the banner
151
+ * wants real bound port info. Splitting further would hide the
152
+ * ordering more than it would simplify anything.
153
+ */
17
154
  async function createServeHandler(routerDef, contextFactory, hooks, options, schemaRegistry, bridge) {
18
- const port = options?.port ?? 3e3;
155
+ const requestedPort = options?.port ?? 3e3;
19
156
  const hostname = options?.hostname ?? "127.0.0.1";
20
157
  const prefix = options?.basePath ? normalizePrefix(options.basePath) : void 0;
21
158
  const httpHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge), routerDef, options ? {
@@ -26,89 +163,42 @@ async function createServeHandler(routerDef, contextFactory, hooks, options, sch
26
163
  schemaRegistry,
27
164
  hooks
28
165
  }, prefix);
29
- const shutdownOpt = options?.gracefulShutdown ?? true;
30
- let gracefulShutdown;
31
- if (typeof shutdownOpt === "object") gracefulShutdown = {
32
- gracefulTimeout: shutdownOpt.timeout,
33
- forceTimeout: shutdownOpt.forceTimeout
34
- };
35
- else gracefulShutdown = shutdownOpt;
36
- const wsExplicitlyDisabled = options?.ws === false;
37
- const wsOpts = typeof options?.ws === "object" ? options.ws : void 0;
38
- const wsEnabled = !wsExplicitlyDisabled && routerHasSubscription(routerDef);
39
166
  const runtime = detectRuntime();
40
- let fetchHandler = httpHandler;
41
- let bunWebsocket;
167
+ const wsEnabled = !(options?.ws === false) && routerHasSubscription(routerDef);
168
+ const wsOpts = typeof options?.ws === "object" ? options.ws : void 0;
42
169
  const bunServerRef = { current: void 0 };
43
- let nodeAttach;
44
- if (wsEnabled) {
45
- const hooksObj = _createWSHooks(routerDef, wsOpts);
46
- if (runtime === "bun") {
47
- const bunAdapter = (await import("crossws/adapters/bun")).default;
48
- const adapter = bunAdapter({ hooks: hooksObj });
49
- bunWebsocket = adapter.websocket;
50
- fetchHandler = (async (req) => {
51
- if (req.headers.get("upgrade") === "websocket" && bunServerRef.current) {
52
- const res = await adapter.handleUpgrade(req, bunServerRef.current);
53
- if (res) return res;
54
- }
55
- return httpHandler(req);
56
- });
57
- } else if (runtime === "deno") {
58
- const denoAdapter = (await import("crossws/adapters/deno")).default;
59
- const adapter = denoAdapter({ hooks: hooksObj });
60
- fetchHandler = (async (req) => {
61
- if (req.headers.get("upgrade") === "websocket") return adapter.handleUpgrade(req, {});
62
- return httpHandler(req);
63
- });
64
- } else nodeAttach = async (httpServer) => {
65
- const { attachWebSocket } = await import("../ws.mjs");
66
- await attachWebSocket(httpServer, routerDef, wsOpts);
67
- };
68
- }
170
+ const wiring = await wireWebSocket(routerDef, httpHandler, wsEnabled, wsOpts, runtime, bunServerRef);
69
171
  const server = await serve({
70
- port,
172
+ port: requestedPort,
71
173
  hostname,
72
- fetch: fetchHandler,
73
- gracefulShutdown,
174
+ fetch: wiring.fetch,
175
+ gracefulShutdown: resolveShutdown(options?.gracefulShutdown),
74
176
  silent: true,
75
- ...options?.http2 && { tls: {
177
+ ...options?.http2 ? { tls: {
76
178
  cert: options.http2.cert,
77
179
  key: options.http2.key
78
- } },
79
- ...bunWebsocket ? { bun: { websocket: bunWebsocket } } : {}
180
+ } } : {},
181
+ ...wiring.bunWebsocket ? { bun: { websocket: wiring.bunWebsocket } } : {}
80
182
  });
81
183
  await server.ready();
82
184
  if (runtime === "bun" && server.bun?.server) bunServerRef.current = server.bun.server;
83
- if (nodeAttach && server.node?.server) await nodeAttach(server.node.server);
84
- let resolvedPort = port;
85
- if (server.node?.server) {
86
- const addr = server.node.server.address();
87
- if (addr && typeof addr === "object") resolvedPort = addr.port;
88
- } else if (server.bun?.server) resolvedPort = server.bun.server.port ?? port;
89
- const protocol = options?.http2 ? "https" : "http";
90
- const rawUrl = server.url || `${protocol}://${hostname}:${resolvedPort}`;
91
- const url = rawUrl.endsWith("/") ? rawUrl.slice(0, -1) : rawUrl;
92
- console.log(`\nSilgi server running at ${url}`);
93
- if (options?.http2) console.log(` HTTP/2 enabled (with HTTP/1.1 fallback)`);
94
- if (wsEnabled) console.log(` WebSocket RPC at ws://${hostname}:${resolvedPort}/_ws (${runtime})`);
95
- if (options?.scalar) console.log(` Scalar API Reference at ${url}/api/reference`);
96
- if (options?.analytics) console.log(` Analytics dashboard at ${url}/api/analytics`);
97
- console.log();
185
+ if (wiring.attachNode && server.node?.server) await wiring.attachNode(server.node.server);
186
+ const { url, port } = resolveUrl(server, requestedPort, hostname, Boolean(options?.http2));
187
+ printBanner(url, hostname, port, runtime, options, wsEnabled);
98
188
  await hooks.callHook("serve:start", {
99
189
  url,
100
- port: resolvedPort,
190
+ port,
101
191
  hostname
102
192
  });
103
193
  return {
104
194
  url,
105
- port: resolvedPort,
195
+ port,
106
196
  hostname,
107
197
  async close(forceCloseConnections = false) {
108
198
  await server.close(forceCloseConnections);
109
199
  await hooks.callHook("serve:stop", {
110
200
  url,
111
- port: resolvedPort,
201
+ port,
112
202
  hostname
113
203
  });
114
204
  }
@@ -4,15 +4,14 @@ interface EventMeta {
4
4
  retry?: number;
5
5
  }
6
6
  /**
7
- * Attach SSE metadata (id, retry) to a value.
7
+ * Attach SSE `id` / `retry` metadata to a yielded value.
8
8
  *
9
- * Only works with object values (arrays, plain objects, etc.).
10
- * Primitives cannot carry metadata wrap them in an object first.
9
+ * Only object-shaped values can carry metadata; primitives cannot be
10
+ * keyed in the `WeakMap` and are returned unchanged. Wrap primitives
11
+ * in a one-field object when you need metadata on them.
11
12
  */
12
13
  declare function withEventMeta<T>(value: T, meta: EventMeta): T;
13
- /**
14
- * Read SSE metadata from a value (if attached).
15
- */
14
+ /** Read SSE metadata previously attached via `withEventMeta`. */
16
15
  declare function getEventMeta(value: unknown): EventMeta | undefined;
17
16
  //#endregion
18
17
  export { EventMeta, getEventMeta, withEventMeta };