diff --git a/sdk/typescript/examples/deno.ts b/sdk/typescript/examples/deno.ts index 82f0ddd2d..4eb91e45d 100644 --- a/sdk/typescript/examples/deno.ts +++ b/sdk/typescript/examples/deno.ts @@ -34,7 +34,6 @@ serve(async (req: Request) => { stream.mergeFragments( `
Hello ${reader.signals.foo}
`, ); - stream.close(); }); } diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index c4506b88f..f2942de36 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -3,7 +3,9 @@ "version": "0.0.1", "description": "TypeScript SDK for Datastar", "scripts": { - "tsc": "tsc" + "check": "deno lint && deno check src/node/node.ts && deno check src/web/deno.ts", + "serve-deno": "deno run -A src/web/deno.ts", + "serve-node": "deno run -A build.ts && node npm/esm/node/node.js" }, "type": "module", "repository": { @@ -17,11 +19,11 @@ }, "homepage": "https://github.com/starfederation/datastar#readme", "dependencies": { - "@types/node": "^22.10.2", - "deepmerge-ts": "^7.1.4", - "type-fest": "^4.32.0" + "deepmerge-ts": "^7.1.4" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "@types/node": "^22.10.2", + "type-fest": "^4.32.0" } } diff --git a/sdk/typescript/src/abstractServerSentEventGenerator.ts b/sdk/typescript/src/abstractServerSentEventGenerator.ts index 21f3e5ec0..c464e0e5a 100644 --- a/sdk/typescript/src/abstractServerSentEventGenerator.ts +++ b/sdk/typescript/src/abstractServerSentEventGenerator.ts @@ -8,9 +8,10 @@ import { MergeSignalsOptions, } from "./types.ts"; -import { DefaultExecuteScriptAttributes } from "./consts.ts"; - -import { DefaultSseRetryDurationMs } from "./consts.ts"; +import { + DefaultExecuteScriptAttributes, + DefaultSseRetryDurationMs, +} from "./consts.ts"; import type { Jsonifiable } from "npm:type-fest"; @@ -78,10 +79,6 @@ export abstract class ServerSentEventGenerator { } private hasDefaultValue(key: string, val: unknown): boolean { - if (key === DefaultExecuteScriptAttributes.split(" ")[0]) { - return val === DefaultExecuteScriptAttributes.split(" ")[1]; - } - if (key in DefaultMapping) { return val === DefaultMapping[key as keyof typeof DefaultMapping]; } @@ -132,17 +129,19 @@ export abstract class ServerSentEventGenerator { /** * Sends a merge signals event. * - * @param data - Data object that will be merged into the client's signals. - * @param options - Additional options for merging. + * @param data - Data object or json string that will be merged into the client's signals. + * @param [options] - Additional options for merging. */ public mergeSignals( - data: Record, + data: Record | string, options?: MergeSignalsOptions, ): ReturnType { const { eventId, retryDuration, ...eventOptions } = options || {} as Partial; + + const signals = typeof data === "string" ? data : JSON.stringify(data); const dataLines = this.eachOptionIsADataLine(eventOptions) - .concat(this.eachNewlineIsADataLine("signals", JSON.stringify(data))); + .concat(this.eachNewlineIsADataLine("signals", signals)); return this.send("datastar-merge-signals", dataLines, { eventId, @@ -153,17 +152,19 @@ export abstract class ServerSentEventGenerator { /** * Sends a remove signals event. * - * @param paths - Array of paths to remove from the client's signals - * @param options - Additional options for removing signals. + * @param paths - An array of paths or a string containing space separated paths. + * @param [options] - Additional options for removing signals. */ public removeSignals( - paths: string[], + paths: string[] | string, options?: DatastarEventOptions, ): ReturnType { const eventOptions = options || {} as DatastarEventOptions; - const dataLines = paths.flatMap((path) => path.split(" ")).map((path) => - `paths ${path}` - ); + const pathsArray = typeof paths === "string" + ? paths.split(" ") + : paths.flatMap((path) => path.split(" ")); + + const dataLines = pathsArray.map((path) => `paths ${path}`); return this.send("datastar-remove-signals", dataLines, eventOptions); } @@ -172,7 +173,7 @@ export abstract class ServerSentEventGenerator { * Executes a script on the client-side. * * @param script - Script code to execute. - * @param options - Additional options for execution. + * @param [options] - Additional options for execution. */ public executeScript( script: string, @@ -184,9 +185,18 @@ export abstract class ServerSentEventGenerator { attributes, ...eventOptions } = options || {} as Partial; - - const attributesDataLines = this.eachOptionIsADataLine(attributes ?? {}) - .map((line) => `attributes ${line}`); + const attributesArray = attributes instanceof Array + ? attributes + : this.eachOptionIsADataLine(attributes ?? {}); + + const attributesDataLines = attributesArray.filter((line) => { + const parts = line.split(" "); + const defaultParts = DefaultExecuteScriptAttributes.split(" "); + if (parts[0] === defaultParts[0] && parts[1]) { + return parts[1] !== defaultParts[1]; + } + return true; + }).map((line) => `attributes ${line}`); const dataLines = attributesDataLines.concat( this.eachOptionIsADataLine(eventOptions), diff --git a/sdk/typescript/src/node/node.ts b/sdk/typescript/src/node/node.ts index 2be165850..b06a5c016 100644 --- a/sdk/typescript/src/node/node.ts +++ b/sdk/typescript/src/node/node.ts @@ -1,3 +1,4 @@ +import { VERSION } from "../consts.ts"; import { createServer } from "node:http"; import { ServerSentEventGenerator } from "./serverSentEventGenerator.ts"; import type { Jsonifiable } from "npm:type-fest"; @@ -5,12 +6,13 @@ import type { Jsonifiable } from "npm:type-fest"; const hostname = "127.0.0.1"; const port = 3000; +// This server is used for testing the node sdk const server = createServer(async (req, res) => { if (req.url === "/") { const headers = new Headers({ "Content-Type": "text/html" }); res.setHeaders(headers); res.end( - `
Hello
`, + `
Hello
`, ); } else if (req.url?.includes("/test")) { const reader = await ServerSentEventGenerator.readSignals(req); diff --git a/sdk/typescript/src/node/serverSentEventGenerator.ts b/sdk/typescript/src/node/serverSentEventGenerator.ts index e9e170b38..ba1f638c1 100644 --- a/sdk/typescript/src/node/serverSentEventGenerator.ts +++ b/sdk/typescript/src/node/serverSentEventGenerator.ts @@ -28,24 +28,20 @@ export class ServerSentEventGenerator extends AbstractSSEGenerator { } /** - * Closes the stream - */ - public close() { - this.res.end(); - } - - /** - * Initializes the server-sent event generator and executes the streamFunc function. + * Initializes the server-sent event generator and executes the onStart callback. * * @param req - The NodeJS request object. * @param res - The NodeJS response object. * @param onStart - A function that will be passed the initialized ServerSentEventGenerator class as it's first parameter. * @param options? - An object that can contain onError and onCancel callbacks as well as a keepalive boolean. * The onAbort callback will be called whenever the request is aborted + * * The onError callback will be called whenever an error is met. If provided, the onAbort callback will also be executed. * If an onError callback is not provided, then the stream will be ended and the error will be thrown up. - * When keepalive is true (default is false), the stream will be kept open indefinitely, - * otherwise it will be closed when the onStart callback finishes. + * + * The stream is always closed after the onStart callback ends. + * If onStart is non blocking, but you still need the stream to stay open after it is called, + * then the keepalive option will maintain it open until the request is aborted by the client. */ static async stream( req: IncomingMessage, diff --git a/sdk/typescript/src/types.ts b/sdk/typescript/src/types.ts index 2c1377ce0..7d96b3e44 100644 --- a/sdk/typescript/src/types.ts +++ b/sdk/typescript/src/types.ts @@ -95,7 +95,7 @@ type ScriptAttributes = { export interface ExecuteScriptOptions extends DatastarEventOptions { [DatastarDatalineAutoRemove]?: boolean; - [DatastarDatalineAttributes]?: ScriptAttributes; + [DatastarDatalineAttributes]?: ScriptAttributes | string[]; } export interface ExecuteScriptEvent { diff --git a/sdk/typescript/src/web/deno.ts b/sdk/typescript/src/web/deno.ts index 3c8d1618a..b9a3cce9e 100644 --- a/sdk/typescript/src/web/deno.ts +++ b/sdk/typescript/src/web/deno.ts @@ -1,13 +1,15 @@ +import { VERSION } from "../consts.ts"; import { serve } from "https://deno.land/std@0.140.0/http/server.ts"; import { ServerSentEventGenerator } from "./serverSentEventGenerator.ts"; import type { Jsonifiable } from "npm:type-fest"; +// This server is used for testing the web standard based sdk serve(async (req: Request) => { const url = new URL(req.url); if (url.pathname === "/") { return new Response( - `
Hello
`, + `
Hello
`, { headers: { "Content-Type": "text/html" }, }, @@ -19,7 +21,6 @@ serve(async (req: Request) => { if (isEventArray(events)) { return ServerSentEventGenerator.stream((stream) => { testEvents(stream, events); - stream.close(); }); } } @@ -28,7 +29,6 @@ serve(async (req: Request) => { stream.mergeFragments('
Merged
'); await delay(5000); stream.mergeFragments('
After 5 seconds
'); - stream.close(); }); } diff --git a/sdk/typescript/src/web/serverSentEventGenerator.ts b/sdk/typescript/src/web/serverSentEventGenerator.ts index b118b394f..79f47c309 100644 --- a/sdk/typescript/src/web/serverSentEventGenerator.ts +++ b/sdk/typescript/src/web/serverSentEventGenerator.ts @@ -22,23 +22,20 @@ export class ServerSentEventGenerator extends AbstractSSEGenerator { } /** - * Closes the ReadableStream - */ - public close() { - this.controller.close(); - } - - /** - * Initializes the server-sent event generator and executes the streamFunc function. + * Initializes the server-sent event generator and executes the onStart callback. * * @param onStart - A function that will be passed the initialized ServerSentEventGenerator class as it's first parameter. * @param options? - An object that can contain options for the Response constructor onError and onCancel callbacks and a keepalive boolean. * The onAbort callback will be called whenever the request is aborted or the stream is cancelled + * * The onError callback will be called whenever an error is met. If provided, the onAbort callback will also be executed. * If an onError callback is not provided, then the stream will be ended and the error will be thrown up. + * * If responseInit is provided, then it will be passed to the Response constructor along with the default headers. - * When keepalive is true (default is false), the stream will be kept open indefinitely, - * otherwise it will be closed when the onStart callback finishes. + * + * The stream is always closed after the onStart callback ends. + * If onStart is non blocking, but you still need the stream to stay open after it is called, + * then the keepalive option will maintain it open until the request is aborted by the client. * * @returns an HTTP Response */ @@ -58,7 +55,7 @@ export class ServerSentEventGenerator extends AbstractSSEGenerator { try { const stream = onStart(generator); if (stream instanceof Promise) await stream; - if (options?.keepalive) { + if (!options?.keepalive) { controller.close(); } } catch (error) {