datastar-ssegen 0.0.2 → 0.1.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.
- package/.github/workflows/publish.yml +2 -3
- package/README.md +3 -5
- package/examples/commonHandlers.js +10 -7
- package/examples/express.example.js +1 -0
- package/examples/hyper-express.example.js +1 -0
- package/index.js +342 -249
- package/jsconfig.json +22 -0
- package/package.json +12 -2
@@ -1,4 +1,3 @@
|
|
1
|
-
# publish to npm
|
2
1
|
name: Publish Package
|
3
2
|
on:
|
4
3
|
push:
|
@@ -8,12 +7,12 @@ on:
|
|
8
7
|
types:
|
9
8
|
- created
|
10
9
|
jobs:
|
11
|
-
publish:
|
10
|
+
publish-npm:
|
12
11
|
runs-on: ubuntu-latest
|
13
12
|
steps:
|
14
13
|
- name: Checkout code
|
15
14
|
uses: actions/checkout@v3
|
16
|
-
- name: Setup Node.js environment
|
15
|
+
- name: Setup Node.js environment for npm
|
17
16
|
uses: actions/setup-node@v3
|
18
17
|
with:
|
19
18
|
node-version: '18'
|
package/README.md
CHANGED
@@ -34,7 +34,7 @@ const app = express();
|
|
34
34
|
app.use(express.json());
|
35
35
|
|
36
36
|
app.get('/qoute', (req,res)=> {
|
37
|
-
const sse = ServerSentEventGenerator
|
37
|
+
const sse = ServerSentEventGenerator(req, res);
|
38
38
|
const qoutes = [
|
39
39
|
"Any app that can be written in JavaScript, will eventually be written in JavaScript. - Jeff Atwood",
|
40
40
|
"JavaScript is the world's most misunderstood programming language. - Douglas Crockford",
|
@@ -46,7 +46,7 @@ const sse = ServerSentEventGenerator.init(req, res);
|
|
46
46
|
res.end();
|
47
47
|
});
|
48
48
|
app.get('/clock', (req, res)=> {
|
49
|
-
const sse = ServerSentEventGenerator
|
49
|
+
const sse = ServerSentEventGenerator(req, res);
|
50
50
|
setInterval(async () => {
|
51
51
|
await sse.MergeFragments(`<div id="clock">Current Time: ${new Date()}</div>`);
|
52
52
|
}, 1000);
|
@@ -84,9 +84,7 @@ Here's a simple HTML page to interact with the server:
|
|
84
84
|
|
85
85
|
The `ServerSentEventGenerator` provides several functions to facilitate communication with connected Datastar clients using Server-Sent Events:
|
86
86
|
|
87
|
-
- **`
|
88
|
-
|
89
|
-
- **`_send(eventType, dataLines, sendOptions)`**: Sends a server-sent event (SSE) to the client. Options include setting an `eventId` and defining `retryDuration`.
|
87
|
+
- **`ServerSentEventGenerator(request, response)`**: Initializes SSE communication with the specified request and response.
|
90
88
|
|
91
89
|
- **`ReadSignals(signals)`**: Reads and merges signals based on HTTP methods with predefined signals, useful for parsing query or body data sent to the server.
|
92
90
|
|
@@ -45,7 +45,7 @@ export const homepage = (name = "") => {
|
|
45
45
|
};
|
46
46
|
|
47
47
|
export const handleQuote = async (req, res) => {
|
48
|
-
const sse = ServerSentEventGenerator
|
48
|
+
const sse = ServerSentEventGenerator(req, res);
|
49
49
|
const qoutes = [
|
50
50
|
"Any app that can be written in JavaScript, will eventually be written in JavaScript. - Jeff Atwood",
|
51
51
|
"JavaScript is the world's most misunderstood programming language. - Douglas Crockford",
|
@@ -58,34 +58,37 @@ export const handleQuote = async (req, res) => {
|
|
58
58
|
};
|
59
59
|
|
60
60
|
export const handleReadSignals = async (req, res) => {
|
61
|
-
|
61
|
+
console.log(req.method, "readSignals?");
|
62
|
+
const sse = ServerSentEventGenerator(req, res);
|
62
63
|
backendStore = await sse.ReadSignals(backendStore);
|
63
64
|
console.log("backendStore updated", backendStore);
|
64
65
|
res.end();
|
65
66
|
};
|
66
67
|
|
67
68
|
export const handleRemoveTrash = async (req, res) => {
|
68
|
-
const sse = ServerSentEventGenerator
|
69
|
+
const sse = ServerSentEventGenerator(req, res);
|
69
70
|
await sse.RemoveFragments("#trash");
|
70
71
|
await sse.MergeSignals({ lastUpdate: Date.now() });
|
71
72
|
res.end();
|
72
73
|
};
|
73
74
|
|
74
75
|
export const handleExecuteScript = async (req, res) => {
|
75
|
-
const sse = ServerSentEventGenerator
|
76
|
-
await sse.ExecuteScript(`console.log("Hello from the backend!")
|
76
|
+
const sse = ServerSentEventGenerator(req, res);
|
77
|
+
await sse.ExecuteScript(`console.log("Hello from the backend!"); //My comment
|
78
|
+
//What avbout this?
|
79
|
+
console.log('second consolelog on new line');`);
|
77
80
|
await sse.MergeSignals({ lastUpdate: Date.now() });
|
78
81
|
res.end();
|
79
82
|
};
|
80
83
|
|
81
84
|
export const handleClock = async function (req, res) {
|
82
|
-
const sse = ServerSentEventGenerator
|
85
|
+
const sse = ServerSentEventGenerator(req, res);
|
83
86
|
setInterval(async () => {
|
84
87
|
await sse.MergeFragments(`<div id="clock">${new Date()}</div>`);
|
85
88
|
}, 1000);
|
86
89
|
};
|
87
90
|
|
88
91
|
export const handleRemoveSignal = async (req, res) => {
|
89
|
-
const sse = ServerSentEventGenerator
|
92
|
+
const sse = ServerSentEventGenerator(req, res);
|
90
93
|
await sse.RemoveSignals(["xyz"]);
|
91
94
|
};
|
package/index.js
CHANGED
@@ -1,273 +1,366 @@
|
|
1
|
+
// @ts-check
|
1
2
|
import url from "url";
|
2
3
|
import querystring from "querystring";
|
3
4
|
|
4
5
|
/**
|
5
|
-
*
|
6
|
-
*
|
6
|
+
* @typedef {object} ServerSentEventMethods
|
7
|
+
* @property {Function} _send - Sends a server-sent event.
|
8
|
+
* @property {Function} ReadSignals - Reads signals based on HTTP methods and merges them with predefined signals.
|
9
|
+
* @property {Function} MergeFragments - Sends a merge fragments event with specified options.
|
10
|
+
* @property {Function} RemoveFragments - Sends a remove fragments event.
|
11
|
+
* @property {Function} MergeSignals - Sends a merge signals event, with merging options.
|
12
|
+
* @property {Function} RemoveSignals - Sends a remove signals event, requires signal paths.
|
13
|
+
* @property {Function} ExecuteScript - Executes a defined script on the client-side.
|
7
14
|
*/
|
8
|
-
export const ServerSentEventGenerator = {
|
9
|
-
/**
|
10
|
-
* Initializes the server-sent event generator.
|
11
|
-
*
|
12
|
-
* @param {object} res - The response object from the framework.
|
13
|
-
* @returns {Object} Methods for manipulating server-sent events.
|
14
|
-
*/
|
15
|
-
init: function (request, response) {
|
16
|
-
return {
|
17
|
-
headersSent: false,
|
18
|
-
req: request,
|
19
|
-
res: response,
|
20
|
-
/**
|
21
|
-
* @typedef {Object} SendOptions
|
22
|
-
* @property {?string} [eventId=null] - Event ID to attach.
|
23
|
-
* @property {number} [retryDuration=1000] - Retry duration in milliseconds.
|
24
|
-
*/
|
25
15
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
* @param {SendOptions} [sendOptions] - Additional options for sending events.
|
32
|
-
*/
|
33
|
-
_send: function (
|
34
|
-
eventType,
|
35
|
-
dataLines,
|
36
|
-
sendOptions = {
|
37
|
-
eventId: null,
|
38
|
-
retryDuration: 1000,
|
39
|
-
}
|
40
|
-
) {
|
41
|
-
//Prepare the message for sending.
|
42
|
-
let data = dataLines.map((line) => `data: ${line}\n`).join("") + "\n";
|
43
|
-
let eventString = "";
|
44
|
-
if (sendOptions.eventId != null) {
|
45
|
-
eventString += `id: ${sendOptions.eventId}\n`;
|
46
|
-
}
|
47
|
-
if (eventType) {
|
48
|
-
eventString += `event: ${eventType}\n`;
|
49
|
-
}
|
50
|
-
eventString += `retry: ${sendOptions.retryDuration}\n`;
|
51
|
-
eventString += data;
|
16
|
+
/**
|
17
|
+
* @typedef {object} SendOptions
|
18
|
+
* @property {number|null} [eventId] - The ID of the event.
|
19
|
+
* @property {number|null} [retryDuration] - Duration in milliseconds to wait before attempting a retry.
|
20
|
+
*/
|
52
21
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
22
|
+
/**
|
23
|
+
* @typedef {object} MergeFragmentsOptions
|
24
|
+
* @property {string|null} [selector] - CSS selector to scope the merge action.
|
25
|
+
* @property {string} [mergeMode=morph] - Mode to use for merging fragments.
|
26
|
+
* @property {number} [settleDuration=300] - Duration for settling the merge.
|
27
|
+
* @property {boolean|null} [useViewTransition] - Use CSS view transitions if supported.
|
28
|
+
* @property {number|null} [eventId] - Event ID for the merge fragments event.
|
29
|
+
* @property {number|null} [retryDuration] - Retry duration for the event.
|
30
|
+
*/
|
61
31
|
|
62
|
-
|
63
|
-
|
32
|
+
/**
|
33
|
+
* @typedef {object} RemoveFragmentsOptions
|
34
|
+
* @property {number} [settleDuration] - Duration for settling the removal.
|
35
|
+
* @property {boolean|null} [useViewTransition] - Use CSS view transitions if supported.
|
36
|
+
* @property {number|null} [eventId] - Event ID for the remove fragments event.
|
37
|
+
* @property {number|null} [retryDuration] - Retry duration for the event.
|
38
|
+
*/
|
64
39
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
ReadSignals: async function (signals) {
|
72
|
-
if (this.req.method === "GET") {
|
73
|
-
// Parse the URL
|
74
|
-
const parsedUrl = url.parse(this.req.url);
|
75
|
-
const parsedQuery = querystring.parse(parsedUrl.query);
|
76
|
-
const datastarParam = parsedQuery.datastar;
|
40
|
+
/**
|
41
|
+
* @typedef {object} MergeSignalsOptions
|
42
|
+
* @property {boolean} [onlyIfMissing=false] - Merge only if the signal is missing.
|
43
|
+
* @property {number|null} [eventId] - Event ID for the merge signals event.
|
44
|
+
* @property {number|null} [retryDuration] - Retry duration for the event.
|
45
|
+
*/
|
77
46
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
47
|
+
/**
|
48
|
+
* @typedef {object} ExecuteScriptOptions
|
49
|
+
* @property {boolean|null} [autoRemove] - Automatically remove the script after execution.
|
50
|
+
* @property {number|null} [eventId] - Event ID for the execute script event.
|
51
|
+
* @property {number|null} [retryDuration] - Retry duration for the event.
|
52
|
+
*/
|
53
|
+
|
54
|
+
/**
|
55
|
+
* @typedef {Object} HttpRequest
|
56
|
+
* @property {string} method - The HTTP method, e.g., 'GET', 'POST', etc.
|
57
|
+
* @property {string} url - The URL of the request.
|
58
|
+
* @property {Object.<string, string>} headers - The HTTP headers.
|
59
|
+
* @property {Object} [body] - The payload of the request.
|
60
|
+
* @property {Function} [json] - Parses the request body
|
61
|
+
* @property {Function} [on] - Adds event handlers to the request
|
62
|
+
*/
|
63
|
+
|
64
|
+
/**
|
65
|
+
* @typedef {Object} HttpResponse
|
66
|
+
* @property {number} statusCode - The HTTP status code, e.g., 200, 404, etc.
|
67
|
+
* @property {Object.<string, string>} headers - The HTTP headers.
|
68
|
+
* @property {Object|string} [body] - The response body.
|
69
|
+
* @property {Function} setHeader - Sets a header on the response
|
70
|
+
* @property {Function} write - Writes to the response body
|
71
|
+
*/
|
72
|
+
|
73
|
+
/**
|
74
|
+
* Initializes the server-sent event generator.
|
75
|
+
*
|
76
|
+
* @param {HttpRequest} request - The request object.
|
77
|
+
* @param {HttpResponse} response - The response object.
|
78
|
+
* @returns {ServerSentEventMethods} Methods for manipulating server-sent events.
|
79
|
+
*/
|
80
|
+
export function ServerSentEventGenerator(request, response) {
|
81
|
+
const generatorMethods = {
|
82
|
+
headersSent: false,
|
83
|
+
req: request,
|
84
|
+
res: response,
|
85
|
+
/**
|
86
|
+
* Sends a server-sent event (SSE) to the client.
|
87
|
+
*
|
88
|
+
* @param {string} eventType - The type of the event.
|
89
|
+
* @param {string[]} dataLines - Lines of data to send.
|
90
|
+
* @param {SendOptions} [sendOptions] - Additional options for sending events.
|
91
|
+
*/
|
92
|
+
_send: function (
|
93
|
+
eventType,
|
94
|
+
dataLines,
|
95
|
+
sendOptions = {
|
96
|
+
eventId: null,
|
97
|
+
retryDuration: 1000,
|
98
|
+
}
|
99
|
+
) {
|
100
|
+
//Prepare the message for sending.
|
101
|
+
let data = dataLines.map((line) => `data: ${line}\n`).join("") + "\n";
|
102
|
+
let eventString = "";
|
103
|
+
eventString += `comment: "dev"\n`;
|
104
|
+
if (sendOptions.eventId != null) {
|
105
|
+
eventString += `id: ${sendOptions.eventId}\n`;
|
106
|
+
}
|
107
|
+
if (eventType) {
|
108
|
+
eventString += `event: ${eventType}\n`;
|
109
|
+
}
|
110
|
+
eventString += `retry: ${sendOptions.retryDuration}\n`;
|
111
|
+
eventString += data;
|
112
|
+
|
113
|
+
//Send Event
|
114
|
+
if (!this.headersSent) {
|
115
|
+
this.res?.setHeader("Cache-Control", "nocache");
|
116
|
+
this.res?.setHeader("Connection", "keep-alive");
|
117
|
+
this.res?.setHeader("Content-Type", "text/event-stream");
|
118
|
+
this.headersSent = true;
|
119
|
+
}
|
120
|
+
this.res.write(eventString);
|
121
|
+
|
122
|
+
return eventString;
|
123
|
+
},
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Reads signals based on HTTP methods and merges them with provided signals.
|
127
|
+
*
|
128
|
+
* @param {object} signals - Predefined signals to merge with.
|
129
|
+
* @returns {Promise<object>} Merged signals object.
|
130
|
+
*/
|
131
|
+
ReadSignals: async function (signals) {
|
132
|
+
if (this.req.method === "GET") {
|
133
|
+
// Parse the URL
|
134
|
+
const parsedUrl = url.parse(this.req.url);
|
135
|
+
const parsedQuery = querystring.parse(parsedUrl.query);
|
136
|
+
const datastarParam = parsedQuery.datastar;
|
137
|
+
|
138
|
+
const query = JSON.parse(datastarParam);
|
139
|
+
return {
|
140
|
+
...signals,
|
141
|
+
...query,
|
142
|
+
};
|
143
|
+
} else {
|
144
|
+
let body = this.req?.body;
|
145
|
+
if (this.req?.json) {
|
146
|
+
body = await this.req.json();
|
147
|
+
}
|
148
|
+
|
149
|
+
if (!body) {
|
150
|
+
body = await new Promise((resolve, reject) => {
|
85
151
|
let chunks = "";
|
86
152
|
this.req.on("data", (chunk) => {
|
87
153
|
chunks += chunk;
|
88
154
|
});
|
89
155
|
this.req.on("end", () => {
|
90
|
-
|
91
|
-
resolve(chunks);
|
156
|
+
resolve(JSON.parse(chunks));
|
92
157
|
});
|
93
158
|
});
|
94
|
-
let parsedBody = {};
|
95
|
-
try {
|
96
|
-
parsedBody = JSON.parse(body);
|
97
|
-
} catch (err) {
|
98
|
-
console.error(
|
99
|
-
"Problem reading signals, could not parse body as JSON."
|
100
|
-
);
|
101
|
-
}
|
102
|
-
console.log("parsed Body", parsedBody);
|
103
|
-
return { ...signals, ...parsedBody };
|
104
159
|
}
|
105
|
-
},
|
106
|
-
/**
|
107
|
-
* @typedef {Object} MergeFragmentsOptions
|
108
|
-
* @property {string} [selector] - CSS selector affected.
|
109
|
-
* @property {string} [mergeMode="morph"] - Mode for merging.
|
110
|
-
* @property {number} [settleDuration=300] - Duration to settle.
|
111
|
-
* @property {?boolean} [useViewTransition=null] - Use view transition.
|
112
|
-
* @property {?string} [eventId=null] - Event ID to attach.
|
113
|
-
* @property {?number} [retryDuration=null] - Retry duration in milliseconds.
|
114
|
-
*/
|
115
160
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
* @param {string[]} fragments - Array of fragment identifiers.
|
120
|
-
* @param {MergeFragmentsOptions} options - Additional options for merging.
|
121
|
-
* @throws Will throw an error if fragments are missing.
|
122
|
-
*/
|
123
|
-
MergeFragments: function (
|
124
|
-
fragments,
|
125
|
-
options = {
|
126
|
-
selector: null,
|
127
|
-
mergeMode: "morph",
|
128
|
-
settleDuration: 300,
|
129
|
-
useViewTransition: null,
|
130
|
-
eventId: null,
|
131
|
-
retryDuration: null,
|
132
|
-
}
|
133
|
-
) {
|
134
|
-
let dataLines = [];
|
135
|
-
if (options?.selector != null)
|
136
|
-
dataLines.push(`selector ${options.selector}`);
|
137
|
-
if (options?.settleDuration != null)
|
138
|
-
dataLines.push(`settleDuration ${options.settleDuration}`);
|
139
|
-
if (options?.useViewTransition != null)
|
140
|
-
dataLines.push(`useViewTransition ${options.useViewTransition}`);
|
141
|
-
if (fragments) {
|
142
|
-
if (typeof fragments === "string") {
|
143
|
-
// Handle case where 'fragments' is a string
|
144
|
-
dataLines.push(`fragments ${fragments.replace(/[\r\n]+/g, "")}`);
|
145
|
-
} else if (Array.isArray(fragments)) {
|
146
|
-
// Handle case where 'fragments' is an array
|
147
|
-
fragments.forEach((frag) => {
|
148
|
-
dataLines.push(`fragments ${frag.replace(/[\r\n]+/g, "")}`);
|
149
|
-
});
|
150
|
-
} else {
|
151
|
-
throw Error(
|
152
|
-
"Invalid type for fragments. Expected string or array."
|
153
|
-
);
|
154
|
-
}
|
155
|
-
} else {
|
156
|
-
throw Error("MergeFragments missing fragment(s).");
|
157
|
-
}
|
158
|
-
return this._send("datastar-merge-fragments", dataLines, {
|
159
|
-
eventId: options?.eventId,
|
160
|
-
retryDuration: options?.retryDuration,
|
161
|
-
});
|
162
|
-
},
|
163
|
-
/**
|
164
|
-
* @typedef {Object} RemoveFragmentsOptions
|
165
|
-
* @property {number} [settleDuration] - Duration to settle.
|
166
|
-
* @property {?boolean} [useViewTransition=null] - Use view transition.
|
167
|
-
* @property {?string} [eventId=null] - Event ID to attach.
|
168
|
-
* @property {?number} [retryDuration=null] - Retry duration in milliseconds.
|
169
|
-
*/
|
161
|
+
return { ...signals, ...body };
|
162
|
+
}
|
163
|
+
},
|
170
164
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
165
|
+
/**
|
166
|
+
* Sends a merge fragments event.
|
167
|
+
*
|
168
|
+
* @param {string[]|string} fragments - Array of fragment identifiers.
|
169
|
+
* @param {MergeFragmentsOptions} options - Additional options for merging.
|
170
|
+
* @throws Will throw an error if fragments are missing.
|
171
|
+
*/
|
172
|
+
MergeFragments: function (
|
173
|
+
fragments,
|
174
|
+
options = {
|
175
|
+
selector: null,
|
176
|
+
mergeMode: "morph",
|
177
|
+
settleDuration: 300,
|
178
|
+
useViewTransition: null,
|
179
|
+
eventId: null,
|
180
|
+
retryDuration: null,
|
181
|
+
}
|
182
|
+
) {
|
183
|
+
let dataLines = [];
|
184
|
+
if (options?.selector != null)
|
185
|
+
dataLines.push(`selector ${options.selector}`);
|
186
|
+
if (options?.settleDuration != null)
|
187
|
+
dataLines.push(`settleDuration ${options.settleDuration}`);
|
188
|
+
if (options?.useViewTransition != null)
|
189
|
+
dataLines.push(`useViewTransition ${options.useViewTransition}`);
|
190
|
+
if (fragments) {
|
191
|
+
if (typeof fragments === "string") {
|
192
|
+
// Handle case where 'fragments' is a string
|
193
|
+
dataLines.push(`fragments ${fragments.replace(/[\r\n]+/g, "")}`);
|
194
|
+
} else if (Array.isArray(fragments)) {
|
195
|
+
// Handle case where 'fragments' is an array
|
196
|
+
fragments.forEach((frag) => {
|
197
|
+
dataLines.push(`fragments ${frag.replace(/[\r\n]+/g, "")}`);
|
198
|
+
});
|
182
199
|
} else {
|
183
|
-
throw Error("
|
200
|
+
throw Error("Invalid type for fragments. Expected string or array.");
|
184
201
|
}
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
202
|
+
} else {
|
203
|
+
throw Error("MergeFragments missing fragment(s).");
|
204
|
+
}
|
205
|
+
return this._send("datastar-merge-fragments", dataLines, {
|
206
|
+
eventId: options?.eventId,
|
207
|
+
retryDuration: options?.retryDuration,
|
208
|
+
});
|
209
|
+
},
|
210
|
+
/**
|
211
|
+
* Sends a remove fragments event.
|
212
|
+
*
|
213
|
+
* @param {string} selector - CSS selector of fragments to remove.
|
214
|
+
* @param {RemoveFragmentsOptions} options - Additional options for removing.
|
215
|
+
* @throws Will throw an error if selector is missing.
|
216
|
+
*/
|
217
|
+
RemoveFragments: function (selector, options) {
|
218
|
+
let dataLines = [];
|
219
|
+
if (selector) {
|
220
|
+
dataLines.push(`selector ${selector}`);
|
221
|
+
} else {
|
222
|
+
throw Error("RemoveFragments missing selector.");
|
223
|
+
}
|
224
|
+
if (options?.settleDuration != null)
|
225
|
+
dataLines.push(`settleDuration ${options.settleDuration}`);
|
226
|
+
if (options?.useViewTransition != null)
|
227
|
+
dataLines.push(`useViewTransition ${options.useViewTransition}`);
|
228
|
+
return this._send(`datastar-remove-fragments`, dataLines, {
|
229
|
+
eventId: options?.eventId,
|
230
|
+
retryDuration: options?.retryDuration,
|
231
|
+
});
|
232
|
+
},
|
200
233
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
})
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
}
|
246
|
-
|
247
|
-
* @typedef {Object} ExecuteScriptOptions
|
248
|
-
* @property {boolean} [autoRemove] - Automatically remove the script after execution.
|
249
|
-
* @property {?string} [eventId=null] - Event ID to attach.
|
250
|
-
* @property {?number} [retryDuration=null] - Retry duration in milliseconds.
|
251
|
-
*/
|
234
|
+
/**
|
235
|
+
* Sends a merge signals event.
|
236
|
+
*
|
237
|
+
* @param {object} signals - Signals to merge.
|
238
|
+
* @param {MergeSignalsOptions} options - Additional options for merging.
|
239
|
+
* @throws Will throw an error if signals are missing.
|
240
|
+
*/
|
241
|
+
MergeSignals: function (signals, options) {
|
242
|
+
let dataLines = [];
|
243
|
+
if (options?.onlyIfMissing === true) {
|
244
|
+
dataLines.push(`onlyIfMissing true`);
|
245
|
+
}
|
246
|
+
if (signals) {
|
247
|
+
dataLines.push(`signals ${JSON.stringify(signals)}`);
|
248
|
+
} else {
|
249
|
+
throw Error("MergeSignals missing signals.");
|
250
|
+
}
|
251
|
+
return this._send(`datastar-merge-signals`, dataLines, {
|
252
|
+
eventId: options?.eventId,
|
253
|
+
retryDuration: options?.retryDuration,
|
254
|
+
});
|
255
|
+
},
|
256
|
+
/**
|
257
|
+
* Sends a remove signals event.
|
258
|
+
*
|
259
|
+
* @param {string[]} paths - Paths of signals to remove.
|
260
|
+
* @param {SendOptions} options - Additional options for removing signals.
|
261
|
+
* @throws Will throw an error if paths are missing.
|
262
|
+
*/
|
263
|
+
RemoveSignals: function (paths, options) {
|
264
|
+
/** @type {Array<string>} */
|
265
|
+
let dataLines = [];
|
266
|
+
if (paths) {
|
267
|
+
paths
|
268
|
+
.map((path) => {
|
269
|
+
dataLines.push(`paths ${path}`);
|
270
|
+
})
|
271
|
+
.join("");
|
272
|
+
} else {
|
273
|
+
throw Error("RemoveSignals missing paths");
|
274
|
+
}
|
275
|
+
return this._send(`datastar-remove-signals`, dataLines, {
|
276
|
+
eventId: options?.eventId,
|
277
|
+
retryDuration: options?.retryDuration,
|
278
|
+
});
|
279
|
+
},
|
252
280
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
281
|
+
/**
|
282
|
+
* Executes a script on the client-side.
|
283
|
+
*
|
284
|
+
* @param {string} script - Script code to execute.
|
285
|
+
* @param {ExecuteScriptOptions} options - Additional options for execution.
|
286
|
+
*/
|
287
|
+
ExecuteScript: function (script, options) {
|
288
|
+
let dataLines = [];
|
289
|
+
if (options?.autoRemove != null) {
|
290
|
+
dataLines.push(`autoRemove ${options.autoRemove}`);
|
291
|
+
}
|
292
|
+
|
293
|
+
if (script) {
|
294
|
+
const lines = script.split("\n");
|
295
|
+
let insideBlockComment = false;
|
296
|
+
const processedLines = lines
|
297
|
+
.map((line) => {
|
298
|
+
line = line.trim();
|
299
|
+
let result = [];
|
300
|
+
let isInString = false;
|
301
|
+
let isEscape = false;
|
302
|
+
|
303
|
+
for (let i = 0; i < line.length; i++) {
|
304
|
+
const char = line[i];
|
305
|
+
|
306
|
+
if (insideBlockComment) {
|
307
|
+
// Check for end of block comment
|
308
|
+
if (char === "*" && line[i + 1] === "/") {
|
309
|
+
insideBlockComment = false;
|
310
|
+
i++; // Skip the '/'
|
311
|
+
}
|
312
|
+
continue;
|
313
|
+
}
|
314
|
+
|
315
|
+
if (isInString) {
|
316
|
+
result.push(char);
|
317
|
+
if (char === "\\" && !isEscape) {
|
318
|
+
isEscape = true; // Detect escaping
|
319
|
+
} else if ((char === '"' || char === "'") && !isEscape) {
|
320
|
+
isInString = false; // End of the string literal
|
321
|
+
} else {
|
322
|
+
isEscape = false;
|
323
|
+
}
|
324
|
+
} else {
|
325
|
+
// Not inside any string
|
326
|
+
if (char === '"' || char === "'") {
|
327
|
+
isInString = true;
|
328
|
+
result.push(char);
|
329
|
+
} else if (char === "/" && line[i + 1] === "/") {
|
330
|
+
break; // Start of single-line comment, ignore rest
|
331
|
+
} else if (char === "/" && line[i + 1] === "*") {
|
332
|
+
insideBlockComment = true; // Begin block comment
|
333
|
+
i++; // Skip the '*'
|
334
|
+
} else {
|
335
|
+
result.push(char);
|
336
|
+
}
|
337
|
+
}
|
338
|
+
}
|
339
|
+
|
340
|
+
// Join the result and ensure it ends with a semicolon if not empty
|
341
|
+
const codePart = result.join("").trim();
|
342
|
+
return codePart && !codePart.endsWith(";")
|
343
|
+
? `${codePart};`
|
344
|
+
: codePart;
|
345
|
+
})
|
346
|
+
.filter((line) => line.length > 0);
|
347
|
+
|
348
|
+
const singleLineScript = processedLines.join(" ");
|
349
|
+
dataLines.push(`script ${singleLineScript}`);
|
350
|
+
}
|
351
|
+
|
352
|
+
return this._send(`datastar-execute-script`, dataLines, {
|
353
|
+
eventId: options?.eventId,
|
354
|
+
retryDuration: options?.retryDuration,
|
355
|
+
});
|
356
|
+
},
|
357
|
+
};
|
358
|
+
return Object.assign(
|
359
|
+
{
|
360
|
+
headersSent: false,
|
361
|
+
req: request,
|
362
|
+
res: response,
|
363
|
+
},
|
364
|
+
generatorMethods
|
365
|
+
);
|
366
|
+
}
|
package/jsconfig.json
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"strict": true,
|
4
|
+
"noImplicitAny": true,
|
5
|
+
"strictFunctionTypes": true,
|
6
|
+
"strictPropertyInitialization": true,
|
7
|
+
"strictBindCallApply": true,
|
8
|
+
"noImplicitThis": true,
|
9
|
+
"noImplicitReturns": true,
|
10
|
+
"alwaysStrict": true,
|
11
|
+
"esModuleInterop": true,
|
12
|
+
"checkJs": true,
|
13
|
+
"allowJs": true,
|
14
|
+
"declaration": true,
|
15
|
+
"target": "ES2016",
|
16
|
+
"module": "ESNext",
|
17
|
+
"moduleResolution": "node",
|
18
|
+
"outDir": "dist"
|
19
|
+
},
|
20
|
+
"include": ["*.js"],
|
21
|
+
"verbose": true
|
22
|
+
}
|
package/package.json
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
{
|
2
2
|
"name": "datastar-ssegen",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.1.1",
|
4
4
|
"description": "Datastar Server-Sent Event generator",
|
5
5
|
"author": "John Cudd",
|
6
6
|
"type": "module",
|
7
7
|
"main": "index.js",
|
8
|
+
"types": "index.d.ts",
|
8
9
|
"scripts": {
|
9
10
|
"dev": "concurrently -c \"red,green,blue\" -n \"node,express,hyper-express\" \"npm run node\" \"npm run express\" \"npm run hyper-express\"",
|
10
11
|
"node": "nodemon examples/node.example.js",
|
11
12
|
"express": "nodemon examples/express.example.js",
|
12
|
-
"hyper-express": "nodemon examples/hyper-express.example.js"
|
13
|
+
"hyper-express": "nodemon examples/hyper-express.example.js",
|
14
|
+
"check-types": "tsc --project ./jsconfig.json"
|
13
15
|
},
|
14
16
|
"keywords": [
|
15
17
|
"datastar",
|
@@ -31,5 +33,13 @@
|
|
31
33
|
"hyper-express": "^6.17.3",
|
32
34
|
"nodemon": "^3.1.9",
|
33
35
|
"npm-run-all": "^4.1.5"
|
36
|
+
},
|
37
|
+
"devDependencies": {
|
38
|
+
"concurrently": "^9.1.0",
|
39
|
+
"typescript": "^5.7.2"
|
40
|
+
},
|
41
|
+
"dependencies": {
|
42
|
+
"express": "^4.21.2",
|
43
|
+
"hyper-express": "^6.17.3"
|
34
44
|
}
|
35
45
|
}
|