From e4e30fc7767e33df915be2aced2e3c29d5d2d77d Mon Sep 17 00:00:00 2001 From: Nick Chomey Date: Fri, 20 Feb 2026 17:57:19 -0600 Subject: [PATCH 1/3] Add namespace support to patchElements; update Deno test to use Deno.serve --- src/abstractServerSentEventGenerator.ts | 21 ++++++++++++++++++++- src/consts.ts | 6 ++++++ src/types.ts | 16 ++++++++++------ test/bun.ts | 3 ++- test/deno.ts | 3 ++- test/node.ts | 3 ++- 6 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/abstractServerSentEventGenerator.ts b/src/abstractServerSentEventGenerator.ts index 1b3cc5f..6432a06 100644 --- a/src/abstractServerSentEventGenerator.ts +++ b/src/abstractServerSentEventGenerator.ts @@ -6,15 +6,18 @@ import { PatchSignalsOptions, Jsonifiable, ElementPatchMode, + NamespaceType, } from "./types.ts"; import { DatastarDatalineElements, DatastarDatalinePatchMode, + DatastarDatalineNamespace, DatastarDatalineSelector, DatastarDatalineSignals, DefaultSseRetryDurationMs, ElementPatchModes, + NamespaceTypes, } from "./consts.ts"; /** @@ -38,6 +41,16 @@ export abstract class ServerSentEventGenerator { } } + /** + * Validates that the provided namespace is a valid NamespaceType. + * @param namespace - The namespace to validate + * @throws {Error} If the namespace is invalid + */ + private validateNamespace(namespace: string): asserts namespace is NamespaceType { + if (!NamespaceTypes.includes(namespace as NamespaceType)) { + throw new Error(`Invalid namespace: "${namespace}". Valid namespaces are: ${NamespaceTypes.join(', ')}`); + } + } /** * Validates required parameters are not empty or undefined. @@ -143,7 +156,7 @@ export abstract class ServerSentEventGenerator { * ``` * * @param elements - HTML string of elements to patch (must have IDs unless using selector). - * @param options - Patch options: selector, mode, useViewTransition, eventId, retryDuration. + * @param options - Patch options: selector, mode, namespace, useViewTransition, eventId, retryDuration. * @returns The SSE lines to send. */ public patchElements( @@ -159,6 +172,12 @@ export abstract class ServerSentEventGenerator { this.validateElementPatchMode(patchMode); } + // Validate namespace if provided + const namespace = (renderOptions as Record)[DatastarDatalineNamespace] as string | undefined; + if (namespace) { + this.validateNamespace(namespace); + } + // Check if we're in remove mode with a selector const selector = (renderOptions as Record)[DatastarDatalineSelector] as string; const isRemoveWithSelector = patchMode === 'remove' && selector; diff --git a/src/consts.ts b/src/consts.ts index 045eb11..dfd4197 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -13,6 +13,7 @@ export const DefaultPatchSignalsOnlyIfMissing = false; export const DatastarDatalineSelector = "selector" export const DatastarDatalinePatchMode = "mode" export const DatastarDatalineElements = "elements" +export const DatastarDatalineNamespace = "namespace" export const DatastarDatalineUseViewTransition = "useViewTransition" export const DatastarDatalineSignals = "signals" export const DatastarDatalineOnlyIfMissing = "onlyIfMissing" @@ -38,6 +39,11 @@ export const ElementPatchModes = [ "remove", ] as const; +export const NamespaceTypes = [ + "svg", + "mathml" +] as const + // Default value for ElementPatchMode export const DefaultElementPatchMode = "outer"; diff --git a/src/types.ts b/src/types.ts index 20e6263..07a80e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,25 +4,28 @@ import { DatastarDatalineOnlyIfMissing, DatastarDatalineSelector, DatastarDatalineSignals, + DatastarDatalineNamespace, DatastarDatalineUseViewTransition, DefaultElementPatchMode, DefaultElementsUseViewTransitions, DefaultPatchSignalsOnlyIfMissing, EventTypes, ElementPatchModes, + NamespaceTypes, } from "./consts.ts"; // Simple Jsonifiable type definition to replace npm:type-fest dependency -export type Jsonifiable = - | string - | number - | boolean - | null +export type Jsonifiable = + | string + | number + | boolean + | null | undefined - | Jsonifiable[] + | Jsonifiable[] | { [key: string]: Jsonifiable }; export type ElementPatchMode = typeof ElementPatchModes[number]; +export type NamespaceType = typeof NamespaceTypes[number]; export type EventType = typeof EventTypes[number]; export type StreamOptions = Partial<{ @@ -44,6 +47,7 @@ export interface ElementOptions extends DatastarEventOptions { export interface PatchElementsOptions extends ElementOptions { [DatastarDatalinePatchMode]?: ElementPatchMode; [DatastarDatalineSelector]?: string; + [DatastarDatalineNamespace]?: NamespaceType; } export interface patchElementsEvent { diff --git a/test/bun.ts b/test/bun.ts index a7e6ec8..dc653c5 100644 --- a/test/bun.ts +++ b/test/bun.ts @@ -75,11 +75,12 @@ function testEvents(stream, events) { const { type, ...e } = event; switch (type) { case "patchElements": { - const { elements, mode, selector, useViewTransition, ...options } = e; + const { elements, mode, selector, useViewTransition, namespace, ...options } = e; const patchOptions = { ...options }; if (mode && mode !== "outer") patchOptions.mode = mode; if (selector) patchOptions.selector = selector; if (useViewTransition !== undefined) patchOptions.useViewTransition = useViewTransition; + if (namespace) patchOptions.namespace = namespace; stream.patchElements(elements || "", patchOptions); break; } diff --git a/test/deno.ts b/test/deno.ts index ebb9833..b2f35fd 100644 --- a/test/deno.ts +++ b/test/deno.ts @@ -77,11 +77,12 @@ function testEvents( const { type, ...e } = event; switch (type) { case "patchElements": { - const { elements, mode, selector, useViewTransition, ...options } = e; + const { elements, mode, selector, useViewTransition, namespace, ...options } = e; const patchOptions: Record = { ...options }; if (mode && mode !== "outer") patchOptions.mode = mode; if (selector) patchOptions.selector = selector; if (useViewTransition !== undefined) patchOptions.useViewTransition = useViewTransition; + if (namespace) patchOptions.namespace = namespace; stream.patchElements((elements as string) || "", patchOptions); break; } diff --git a/test/node.ts b/test/node.ts index 8e2aae0..de97d5b 100644 --- a/test/node.ts +++ b/test/node.ts @@ -82,11 +82,12 @@ function testEvents( const { type, ...e } = event; switch (type) { case "patchElements": { - const { elements, mode, selector, useViewTransition, ...options } = e; + const { elements, mode, selector, useViewTransition, namespace, ...options } = e; const patchOptions: Record = { ...options }; if (mode && mode !== "outer") patchOptions.mode = mode; if (selector) patchOptions.selector = selector; if (useViewTransition !== undefined) patchOptions.useViewTransition = useViewTransition; + if (namespace) patchOptions.namespace = namespace; stream.patchElements((elements as string) || "", patchOptions); break; } From 0bb2678962cbc43b9b34d96b4e2d1a06b629f37c Mon Sep 17 00:00:00 2001 From: Nick Chomey Date: Fri, 20 Feb 2026 17:57:19 -0600 Subject: [PATCH 2/3] update Deno test and example to use Deno.serve --- examples/deno/deno.ts | 3 +-- test/deno.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/deno/deno.ts b/examples/deno/deno.ts index ca65e30..861e0f9 100644 --- a/examples/deno/deno.ts +++ b/examples/deno/deno.ts @@ -1,7 +1,6 @@ -import { serve } from "https://deno.land/std@0.140.0/http/server.ts"; import { ServerSentEventGenerator } from "npm:@starfederation/datastar-sdk/web"; -serve(async (req: Request) => { +Deno.serve(async (req: Request) => { const url = new URL(req.url); if (url.pathname === "/") { diff --git a/test/deno.ts b/test/deno.ts index b2f35fd..b07b247 100644 --- a/test/deno.ts +++ b/test/deno.ts @@ -1,9 +1,8 @@ -import { serve } from "https://deno.land/std@0.140.0/http/server.ts"; import { ServerSentEventGenerator } from "../npm/esm/web/serverSentEventGenerator.js"; import type { Jsonifiable } from "../src/types.ts"; // This server is used for testing the web standard based sdk -serve(async (req: Request) => { +Deno.serve(async (req: Request) => { const url = new URL(req.url); if (url.pathname === "/") { From 741e8f24f1127e24fcb6f634486fbde2b16c1adf Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:20:47 -0600 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/abstractServerSentEventGenerator.ts | 6 +++++- src/consts.ts | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/abstractServerSentEventGenerator.ts b/src/abstractServerSentEventGenerator.ts index 6432a06..185fa25 100644 --- a/src/abstractServerSentEventGenerator.ts +++ b/src/abstractServerSentEventGenerator.ts @@ -174,7 +174,11 @@ export abstract class ServerSentEventGenerator { // Validate namespace if provided const namespace = (renderOptions as Record)[DatastarDatalineNamespace] as string | undefined; - if (namespace) { + const hasNamespace = DatastarDatalineNamespace in (renderOptions as Record); + if (hasNamespace) { + if (typeof namespace !== "string" || namespace.trim() === "") { + throw new Error("Namespace, if provided, must be a non-empty string"); + } this.validateNamespace(namespace); } diff --git a/src/consts.ts b/src/consts.ts index dfd4197..3d36660 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -41,8 +41,8 @@ export const ElementPatchModes = [ export const NamespaceTypes = [ "svg", - "mathml" -] as const + "mathml", +] as const; // Default value for ElementPatchMode export const DefaultElementPatchMode = "outer";