Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typescript sdk standardization #700

Merged
Merged
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
1 change: 0 additions & 1 deletion sdk/typescript/examples/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ serve(async (req: Request) => {
stream.mergeFragments(
`<div id="toMerge">Hello ${reader.signals.foo}</div>`,
);
stream.close();
});
}

Expand Down
12 changes: 7 additions & 5 deletions sdk/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
}
}
52 changes: 31 additions & 21 deletions sdk/typescript/src/abstractServerSentEventGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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];
}
Expand Down Expand Up @@ -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<string, Jsonifiable>,
data: Record<string, Jsonifiable> | string,
options?: MergeSignalsOptions,
): ReturnType<typeof this.send> {
const { eventId, retryDuration, ...eventOptions } = options ||
{} as Partial<MergeSignalsOptions>;

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,
Expand All @@ -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<typeof this.send> {
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);
}
Expand All @@ -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,
Expand All @@ -184,9 +185,18 @@ export abstract class ServerSentEventGenerator {
attributes,
...eventOptions
} = options || {} as Partial<ExecuteScriptOptions>;

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),
Expand Down
4 changes: 3 additions & 1 deletion sdk/typescript/src/node/node.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { VERSION } from "../consts.ts";
import { createServer } from "node:http";
import { ServerSentEventGenerator } from "./serverSentEventGenerator.ts";
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(
`<html><head><script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.8/bundles/datastar.js"></script></head><body><div id="toMerge" data-signals-foo="'World'" data-on-load="@get('/merge')">Hello</div></body></html>`,
`<html><head><script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v${VERSION}/bundles/datastar.js"></script></head><body><div id="toMerge" data-signals-foo="'World'" data-on-load="@get('/merge')">Hello</div></body></html>`,
);
} else if (req.url?.includes("/test")) {
const reader = await ServerSentEventGenerator.readSignals(req);
Expand Down
16 changes: 6 additions & 10 deletions sdk/typescript/src/node/serverSentEventGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ type ScriptAttributes = {

export interface ExecuteScriptOptions extends DatastarEventOptions {
[DatastarDatalineAutoRemove]?: boolean;
[DatastarDatalineAttributes]?: ScriptAttributes;
[DatastarDatalineAttributes]?: ScriptAttributes | string[];
}

export interface ExecuteScriptEvent {
Expand Down
6 changes: 3 additions & 3 deletions sdk/typescript/src/web/deno.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { VERSION } from "../consts.ts";
import { serve } from "https://deno.land/[email protected]/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(
`<html><head><script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.8/bundles/datastar.js"></script></head><body><div id="toMerge" data-signals-foo="'World'" data-on-load="@get('/merge')">Hello</div></body></html>`,
`<html><head><script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v${VERSION}/bundles/datastar.js"></script></head><body><div id="toMerge" data-signals-foo="'World'" data-on-load="@get('/merge')">Hello</div></body></html>`,
{
headers: { "Content-Type": "text/html" },
},
Expand All @@ -19,7 +21,6 @@ serve(async (req: Request) => {
if (isEventArray(events)) {
return ServerSentEventGenerator.stream((stream) => {
testEvents(stream, events);
stream.close();
});
}
}
Expand All @@ -28,7 +29,6 @@ serve(async (req: Request) => {
stream.mergeFragments('<div id="toMerge">Merged</div>');
await delay(5000);
stream.mergeFragments('<div id="toMerge">After 5 seconds</div>');
stream.close();
});
}

Expand Down
19 changes: 8 additions & 11 deletions sdk/typescript/src/web/serverSentEventGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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) {
Expand Down