datastar-ssegen 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.github/workflows/publish.yml +2 -3
- package/README.md +3 -3
- 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,7 +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
|
-
- **`
|
87
|
+
- **`ServerSentEventGenerator(request, response)`**: Initializes SSE communication with the specified request and response.
|
88
88
|
|
89
89
|
- **`_send(eventType, dataLines, sendOptions)`**: Sends a server-sent event (SSE) to the client. Options include setting an `eventId` and defining `retryDuration`.
|
90
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.0
|
3
|
+
"version": "0.1.0",
|
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
|
}
|