Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions examples/deno/deno.ts
Original file line number Diff line number Diff line change
@@ -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 === "/") {
Expand Down
25 changes: 24 additions & 1 deletion src/abstractServerSentEventGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import {
PatchSignalsOptions,
Jsonifiable,
ElementPatchMode,
NamespaceType,
} from "./types.ts";

import {
DatastarDatalineElements,
DatastarDatalinePatchMode,
DatastarDatalineNamespace,
DatastarDatalineSelector,
DatastarDatalineSignals,
DefaultSseRetryDurationMs,
ElementPatchModes,
NamespaceTypes,
} from "./consts.ts";

/**
Expand All @@ -38,6 +41,16 @@ export abstract class ServerSentEventGenerator<T = string[]> {
}
}

/**
* 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.
Expand Down Expand Up @@ -143,7 +156,7 @@ export abstract class ServerSentEventGenerator<T = string[]> {
* ```
*
* @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(
Expand All @@ -159,6 +172,16 @@ export abstract class ServerSentEventGenerator<T = string[]> {
this.validateElementPatchMode(patchMode);
}

// Validate namespace if provided
const namespace = (renderOptions as Record<string, unknown>)[DatastarDatalineNamespace] as string | undefined;
const hasNamespace = DatastarDatalineNamespace in (renderOptions as Record<string, unknown>);
if (hasNamespace) {
if (typeof namespace !== "string" || namespace.trim() === "") {
throw new Error("Namespace, if provided, must be a non-empty string");
}
this.validateNamespace(namespace);
}

// Check if we're in remove mode with a selector
const selector = (renderOptions as Record<string, unknown>)[DatastarDatalineSelector] as string;
const isRemoveWithSelector = patchMode === 'remove' && selector;
Expand Down
6 changes: 6 additions & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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";

Expand Down
16 changes: 10 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand All @@ -44,6 +47,7 @@ export interface ElementOptions extends DatastarEventOptions {
export interface PatchElementsOptions extends ElementOptions {
[DatastarDatalinePatchMode]?: ElementPatchMode;
[DatastarDatalineSelector]?: string;
[DatastarDatalineNamespace]?: NamespaceType;
}

export interface patchElementsEvent {
Expand Down
3 changes: 2 additions & 1 deletion test/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions test/deno.ts
Original file line number Diff line number Diff line change
@@ -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 === "/") {
Expand Down Expand Up @@ -77,11 +76,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<string, unknown> = { ...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;
}
Expand Down
3 changes: 2 additions & 1 deletion test/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = { ...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;
}
Expand Down