@jsenv/server 13.0.0 → 14.1.0
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/README.md +18 -51
- package/package.json +3 -2
- package/src/fetch_filesystem.js +2 -2
- package/src/internal/hostname_parser.js +48 -0
- package/src/internal/listen.js +11 -11
- package/src/internal/nodeStreamToObservable.js +1 -1
- package/src/internal/request_factory.js +16 -14
- package/src/internal/server-polyglot.js +1 -0
- package/src/internal/server_ips.js +49 -0
- package/src/main.js +2 -2
- package/src/service_controller.js +1 -0
- package/src/services/request_aliases/jsenv_service_request_aliases.js +37 -0
- package/src/sse/sse_room.js +1 -3
- package/src/start_server.js +1045 -0
- package/src/internal/getServerOrigins.js +0 -74
- package/src/server.js +0 -896
- package/src/services/ressource_aliases/jsenv_service_ressource_aliases.js +0 -40
package/README.md
CHANGED
|
@@ -24,34 +24,7 @@ await startServer({
|
|
|
24
24
|
})
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
_Code starting a server and using [node-fetch](https://github.com/node-fetch/node-fetch) to send a request on that server:_
|
|
30
|
-
|
|
31
|
-
```js
|
|
32
|
-
import fetch from "node-fetch"
|
|
33
|
-
import { startServer } from "@jsenv/server"
|
|
34
|
-
|
|
35
|
-
const server = await startServer({
|
|
36
|
-
services: [
|
|
37
|
-
{
|
|
38
|
-
handleRequest: () => {
|
|
39
|
-
return {
|
|
40
|
-
status: 200,
|
|
41
|
-
headers: {
|
|
42
|
-
"content-type": "text/plain",
|
|
43
|
-
},
|
|
44
|
-
body: "Hello world",
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
const response = await fetch(server.origin)
|
|
52
|
-
const responseBodyAsText = await response.text()
|
|
53
|
-
console.log(responseBodyAsText) // "Hello world"
|
|
54
|
-
```
|
|
27
|
+
# Examples
|
|
55
28
|
|
|
56
29
|
_Code starting a server with 2 request handlers:_
|
|
57
30
|
|
|
@@ -70,10 +43,10 @@ const server = await startServer({
|
|
|
70
43
|
{
|
|
71
44
|
name: "index",
|
|
72
45
|
handleRequest: (request) => {
|
|
73
|
-
if (request.
|
|
46
|
+
if (request.resource === "/") {
|
|
74
47
|
return { status: 200 }
|
|
75
48
|
}
|
|
76
|
-
return null
|
|
49
|
+
return null
|
|
77
50
|
},
|
|
78
51
|
},
|
|
79
52
|
{
|
|
@@ -101,8 +74,8 @@ import { startServer } from "@jsenv/server"
|
|
|
101
74
|
|
|
102
75
|
await startServer({
|
|
103
76
|
protocol: "https",
|
|
104
|
-
certificate:
|
|
105
|
-
privateKey:
|
|
77
|
+
certificate: readFileSync(new URL("./server.crt", import.meta.url), "utf8"),
|
|
78
|
+
privateKey: readFileSync(new URL("./server.key", import.meta.url), "utf8"),
|
|
106
79
|
allowHttpRequestOnHttps: true,
|
|
107
80
|
services: [
|
|
108
81
|
{
|
|
@@ -120,11 +93,6 @@ await startServer({
|
|
|
120
93
|
},
|
|
121
94
|
],
|
|
122
95
|
})
|
|
123
|
-
|
|
124
|
-
function readFileSyncAsString(relativeUrl) {
|
|
125
|
-
const fileUrl = new URL(relativeUrl, import.meta.url)
|
|
126
|
-
return String(readFileSync(fileUrl))
|
|
127
|
-
}
|
|
128
96
|
```
|
|
129
97
|
|
|
130
98
|
_Code starting a server for static files:_
|
|
@@ -136,10 +104,8 @@ await startServer({
|
|
|
136
104
|
services: [
|
|
137
105
|
{
|
|
138
106
|
handleRequest: async (request) => {
|
|
139
|
-
const fileUrl = new URL(request.
|
|
140
|
-
const response = await fetchFileSystem(fileUrl,
|
|
141
|
-
...request,
|
|
142
|
-
})
|
|
107
|
+
const fileUrl = new URL(request.resource.slice(1), import.meta.url)
|
|
108
|
+
const response = await fetchFileSystem(fileUrl, request)
|
|
143
109
|
return response
|
|
144
110
|
},
|
|
145
111
|
},
|
|
@@ -149,16 +115,17 @@ await startServer({
|
|
|
149
115
|
|
|
150
116
|
# Documentation
|
|
151
117
|
|
|
152
|
-
- [Handling requests](./docs/handling_requests
|
|
153
|
-
- [Handling errors](./docs/handling_errors
|
|
154
|
-
- [Server timing](./docs/server_timing
|
|
155
|
-
- [CORS](./docs/cors
|
|
156
|
-
- [https](./docs/https
|
|
157
|
-
- [Serving files](./docs/serving_files
|
|
158
|
-
- [Content negotiation](./docs/content_negotiation
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
161
|
-
- [
|
|
118
|
+
- [Handling requests](./docs/handling_requests.md)
|
|
119
|
+
- [Handling errors](./docs/handling_errors.md)
|
|
120
|
+
- [Server timing](./docs/server_timing.md)
|
|
121
|
+
- [CORS](./docs/cors.md)
|
|
122
|
+
- [https](./docs/https.md)
|
|
123
|
+
- [Serving files](./docs/serving_files.md)
|
|
124
|
+
- [Content negotiation](./docs/content_negotiation.md)
|
|
125
|
+
- [Websocket](./docs/websocket.md)
|
|
126
|
+
- [Server Sent Events](./docs/server_sent_events.md)
|
|
127
|
+
- [Cluster](./docs/cluster.md)
|
|
128
|
+
- [Http2 push](./docs/http2_push.md)
|
|
162
129
|
|
|
163
130
|
# Installation
|
|
164
131
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.1.0",
|
|
4
4
|
"description": "Write your Node.js server using pure functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"@jsenv/abort": "4.2.3",
|
|
38
38
|
"@jsenv/log": "3.1.0",
|
|
39
39
|
"@jsenv/url-meta": "7.0.0",
|
|
40
|
-
"@jsenv/utils": "2.0.1"
|
|
40
|
+
"@jsenv/utils": "2.0.1",
|
|
41
|
+
"ws": "8.8.1"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@jsenv/fetch": "../fetch/",
|
package/src/fetch_filesystem.js
CHANGED
|
@@ -64,7 +64,7 @@ export const fetchFileSystem = async (
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// here you might be tempted to add || cacheControl === 'no-cache'
|
|
67
|
-
// but no-cache means
|
|
67
|
+
// but no-cache means resource can be cache but must be revalidated (yeah naming is strange)
|
|
68
68
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Cacheability
|
|
69
69
|
if (cacheControl === "no-store") {
|
|
70
70
|
if (etagEnabled) {
|
|
@@ -206,7 +206,7 @@ const getClientCacheResponse = async ({
|
|
|
206
206
|
sourceUrl,
|
|
207
207
|
}) => {
|
|
208
208
|
// here you might be tempted to add || headers["cache-control"] === "no-cache"
|
|
209
|
-
// but no-cache means
|
|
209
|
+
// but no-cache means resource can be cache but must be revalidated (yeah naming is strange)
|
|
210
210
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Cacheability
|
|
211
211
|
|
|
212
212
|
if (
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isIP } from "node:net"
|
|
2
|
+
|
|
3
|
+
export const parseHostname = (hostname) => {
|
|
4
|
+
if (hostname === "0.0.0.0") {
|
|
5
|
+
return {
|
|
6
|
+
type: "ip",
|
|
7
|
+
label: "unspecified",
|
|
8
|
+
version: 4,
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (
|
|
12
|
+
hostname === "::" ||
|
|
13
|
+
hostname === "0000:0000:0000:0000:0000:0000:0000:0000"
|
|
14
|
+
) {
|
|
15
|
+
return {
|
|
16
|
+
type: "ip",
|
|
17
|
+
label: "unspecified",
|
|
18
|
+
version: 6,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (hostname === "127.0.0.1") {
|
|
22
|
+
return {
|
|
23
|
+
type: "ip",
|
|
24
|
+
label: "loopback",
|
|
25
|
+
version: 4,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
hostname === "::1" ||
|
|
30
|
+
hostname === "0000:0000:0000:0000:0000:0000:0000:0001"
|
|
31
|
+
) {
|
|
32
|
+
return {
|
|
33
|
+
type: "ip",
|
|
34
|
+
label: "loopback",
|
|
35
|
+
version: 6,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const ipVersion = isIP(hostname)
|
|
39
|
+
if (ipVersion === 0) {
|
|
40
|
+
return {
|
|
41
|
+
type: "hostname",
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: "ip",
|
|
46
|
+
version: ipVersion,
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/internal/listen.js
CHANGED
|
@@ -6,7 +6,7 @@ const listen = async ({
|
|
|
6
6
|
server,
|
|
7
7
|
port,
|
|
8
8
|
portHint,
|
|
9
|
-
|
|
9
|
+
hostname,
|
|
10
10
|
}) => {
|
|
11
11
|
const listeningOperation = Abort.startOperation()
|
|
12
12
|
|
|
@@ -17,11 +17,11 @@ const listen = async ({
|
|
|
17
17
|
listeningOperation.throwIfAborted()
|
|
18
18
|
port = await findFreePort(portHint, {
|
|
19
19
|
signal: listeningOperation.signal,
|
|
20
|
-
|
|
20
|
+
hostname,
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
listeningOperation.throwIfAborted()
|
|
24
|
-
port = await startListening({ server, port,
|
|
24
|
+
port = await startListening({ server, port, hostname })
|
|
25
25
|
listeningOperation.addAbortCallback(() => stopListening(server))
|
|
26
26
|
listeningOperation.throwIfAborted()
|
|
27
27
|
|
|
@@ -35,7 +35,7 @@ export const findFreePort = async (
|
|
|
35
35
|
initialPort = 1,
|
|
36
36
|
{
|
|
37
37
|
signal = new AbortController().signal,
|
|
38
|
-
|
|
38
|
+
hostname = "127.0.0.1",
|
|
39
39
|
min = 1,
|
|
40
40
|
max = 65534,
|
|
41
41
|
next = (port) => port + 1,
|
|
@@ -56,26 +56,26 @@ export const findFreePort = async (
|
|
|
56
56
|
const nextPort = next(port)
|
|
57
57
|
if (nextPort > max) {
|
|
58
58
|
throw new Error(
|
|
59
|
-
`${
|
|
59
|
+
`${hostname} has no available port between ${min} and ${max}`,
|
|
60
60
|
)
|
|
61
61
|
}
|
|
62
|
-
return testUntil(nextPort,
|
|
62
|
+
return testUntil(nextPort, hostname)
|
|
63
63
|
}
|
|
64
|
-
const freePort = await testUntil(initialPort,
|
|
64
|
+
const freePort = await testUntil(initialPort, hostname)
|
|
65
65
|
return freePort
|
|
66
66
|
} finally {
|
|
67
67
|
await findFreePortOperation.end()
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const portIsFree = async (port,
|
|
71
|
+
const portIsFree = async (port, hostname) => {
|
|
72
72
|
const server = createServer()
|
|
73
73
|
|
|
74
74
|
try {
|
|
75
75
|
await startListening({
|
|
76
76
|
server,
|
|
77
77
|
port,
|
|
78
|
-
|
|
78
|
+
hostname,
|
|
79
79
|
})
|
|
80
80
|
} catch (error) {
|
|
81
81
|
if (error && error.code === "EADDRINUSE") {
|
|
@@ -91,7 +91,7 @@ const portIsFree = async (port, host) => {
|
|
|
91
91
|
return true
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const startListening = ({ server, port,
|
|
94
|
+
const startListening = ({ server, port, hostname }) => {
|
|
95
95
|
return new Promise((resolve, reject) => {
|
|
96
96
|
server.on("error", reject)
|
|
97
97
|
server.on("listening", () => {
|
|
@@ -99,7 +99,7 @@ const startListening = ({ server, port, host }) => {
|
|
|
99
99
|
// https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
|
|
100
100
|
resolve(server.address().port)
|
|
101
101
|
})
|
|
102
|
-
server.listen(port,
|
|
102
|
+
server.listen(port, hostname)
|
|
103
103
|
})
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -38,7 +38,7 @@ export const nodeStreamToObservable = (nodeStream) => {
|
|
|
38
38
|
// used in the next ${readableStreamLifetimeInSeconds} otherwise destroys it
|
|
39
39
|
const timeout = setTimeout(() => {
|
|
40
40
|
process.emitWarning(
|
|
41
|
-
`Readable stream not used after ${readableStreamLifetimeInSeconds} seconds. It will be destroyed to release
|
|
41
|
+
`Readable stream not used after ${readableStreamLifetimeInSeconds} seconds. It will be destroyed to release resources`,
|
|
42
42
|
{
|
|
43
43
|
CODE: "READABLE_STREAM_TIMEOUT",
|
|
44
44
|
// url is for http client request
|
|
@@ -6,7 +6,9 @@ export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
|
|
|
6
6
|
const body = nodeStreamToObservable(nodeRequest)
|
|
7
7
|
|
|
8
8
|
let requestOrigin
|
|
9
|
-
if (nodeRequest.
|
|
9
|
+
if (nodeRequest.upgrade) {
|
|
10
|
+
requestOrigin = serverOrigin
|
|
11
|
+
} else if (nodeRequest.authority) {
|
|
10
12
|
requestOrigin = nodeRequest.connection.encrypted
|
|
11
13
|
? `https://${nodeRequest.authority}`
|
|
12
14
|
: `http://${nodeRequest.authority}`
|
|
@@ -22,8 +24,8 @@ export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
|
|
|
22
24
|
signal,
|
|
23
25
|
http2: Boolean(nodeRequest.stream),
|
|
24
26
|
origin: requestOrigin,
|
|
25
|
-
...
|
|
26
|
-
|
|
27
|
+
...getPropertiesFromResource({
|
|
28
|
+
resource: nodeRequest.url,
|
|
27
29
|
baseUrl: requestOrigin,
|
|
28
30
|
}),
|
|
29
31
|
method: nodeRequest.method,
|
|
@@ -34,13 +36,13 @@ export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
|
|
|
34
36
|
|
|
35
37
|
export const applyRedirectionToRequest = (
|
|
36
38
|
request,
|
|
37
|
-
{
|
|
39
|
+
{ resource, pathname, ...rest },
|
|
38
40
|
) => {
|
|
39
41
|
return {
|
|
40
42
|
...request,
|
|
41
|
-
...(
|
|
42
|
-
?
|
|
43
|
-
|
|
43
|
+
...(resource
|
|
44
|
+
? getPropertiesFromResource({
|
|
45
|
+
resource,
|
|
44
46
|
baseUrl: request.url,
|
|
45
47
|
})
|
|
46
48
|
: pathname
|
|
@@ -53,20 +55,20 @@ export const applyRedirectionToRequest = (
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
const
|
|
57
|
-
const urlObject = new URL(
|
|
58
|
+
const getPropertiesFromResource = ({ resource, baseUrl }) => {
|
|
59
|
+
const urlObject = new URL(resource, baseUrl)
|
|
58
60
|
let pathname = urlObject.pathname
|
|
59
61
|
|
|
60
62
|
return {
|
|
61
63
|
url: String(urlObject),
|
|
62
64
|
pathname,
|
|
63
|
-
|
|
65
|
+
resource,
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
const getPropertiesFromPathname = ({ pathname, baseUrl }) => {
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
+
return getPropertiesFromResource({
|
|
71
|
+
resource: `${pathname}${new URL(baseUrl).search}`,
|
|
70
72
|
baseUrl,
|
|
71
73
|
})
|
|
72
74
|
}
|
|
@@ -93,12 +95,12 @@ export const createPushRequest = (request, { signal, pathname, method }) => {
|
|
|
93
95
|
const getHeadersInheritedByPushRequest = (request) => {
|
|
94
96
|
const headersInherited = { ...request.headers }
|
|
95
97
|
// mtime sent by the client in request headers concerns the main request
|
|
96
|
-
// Time remains valid for request to other
|
|
98
|
+
// Time remains valid for request to other resources so we keep it
|
|
97
99
|
// in child requests
|
|
98
100
|
// delete childHeaders["if-modified-since"]
|
|
99
101
|
|
|
100
102
|
// eTag sent by the client in request headers concerns the main request
|
|
101
|
-
// A request made to an other
|
|
103
|
+
// A request made to an other resource must not inherit the eTag
|
|
102
104
|
delete headersInherited["if-none-match"]
|
|
103
105
|
|
|
104
106
|
return headersInherited
|
|
@@ -62,6 +62,7 @@ export const createPolyglotServer = async ({
|
|
|
62
62
|
|
|
63
63
|
// The async part is just to lazyly import "http2" or "https"
|
|
64
64
|
// so that these module are parsed only if used.
|
|
65
|
+
// https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions
|
|
65
66
|
const createSecureServer = async ({
|
|
66
67
|
certificate,
|
|
67
68
|
privateKey,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { networkInterfaces } from "node:os"
|
|
2
|
+
|
|
3
|
+
export const createIpGetters = () => {
|
|
4
|
+
const networkAddresses = []
|
|
5
|
+
const networkInterfaceMap = networkInterfaces()
|
|
6
|
+
for (const key of Object.keys(networkInterfaceMap)) {
|
|
7
|
+
for (const networkAddress of networkInterfaceMap[key]) {
|
|
8
|
+
networkAddresses.push(networkAddress)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
getFirstInternalIp: ({ preferIpv6 }) => {
|
|
13
|
+
const isPref = preferIpv6 ? isIpV6 : isIpV4
|
|
14
|
+
let firstInternalIp
|
|
15
|
+
for (const networkAddress of networkAddresses) {
|
|
16
|
+
if (networkAddress.internal) {
|
|
17
|
+
firstInternalIp = networkAddress.address
|
|
18
|
+
if (isPref(networkAddress)) {
|
|
19
|
+
break
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return firstInternalIp
|
|
24
|
+
},
|
|
25
|
+
getFirstExternalIp: ({ preferIpv6 }) => {
|
|
26
|
+
const isPref = preferIpv6 ? isIpV6 : isIpV4
|
|
27
|
+
let firstExternalIp
|
|
28
|
+
for (const networkAddress of networkAddresses) {
|
|
29
|
+
if (!networkAddress.internal) {
|
|
30
|
+
firstExternalIp = networkAddress.address
|
|
31
|
+
if (isPref(networkAddress)) {
|
|
32
|
+
break
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return firstExternalIp
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isIpV4 = (networkAddress) => {
|
|
42
|
+
// node 18.5
|
|
43
|
+
if (typeof networkAddress.family === "number") {
|
|
44
|
+
return networkAddress.family === 4
|
|
45
|
+
}
|
|
46
|
+
return networkAddress.family === "IPv4"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isIpV6 = (networkAddress) => !isIpV4(networkAddress)
|
package/src/main.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Ideally this file should be kept simple to help discovering codebase progressively.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export { startServer } from "./
|
|
7
|
+
export { startServer } from "./start_server.js"
|
|
8
8
|
export { setupRoutes } from "./service_composition/routing.js"
|
|
9
9
|
export { readRequestBody } from "./readRequestBody.js"
|
|
10
10
|
export { fetchFileSystem } from "./fetch_filesystem.js"
|
|
@@ -42,5 +42,5 @@ export { jsenvServiceResponseAcceptanceCheck } from "./services/response_accepta
|
|
|
42
42
|
export { serveDirectory } from "./serve_directory.js"
|
|
43
43
|
export { fromFetchResponse } from "./from_fetch_response.js"
|
|
44
44
|
export { composeTwoResponses } from "./internal/response_composition.js"
|
|
45
|
-
export {
|
|
45
|
+
export { jsenvServiceRequestAliases } from "./services/request_aliases/jsenv_service_request_aliases.js"
|
|
46
46
|
export { findFreePort } from "./internal/listen.js"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { URL_META } from "@jsenv/url-meta"
|
|
2
|
+
|
|
3
|
+
export const jsenvServiceRequestAliases = (resourceAliases) => {
|
|
4
|
+
const aliases = {}
|
|
5
|
+
Object.keys(resourceAliases).forEach((key) => {
|
|
6
|
+
aliases[asFileUrl(key)] = asFileUrl(resourceAliases[key])
|
|
7
|
+
})
|
|
8
|
+
return {
|
|
9
|
+
name: "jsenv:request_aliases",
|
|
10
|
+
redirectRequest: (request) => {
|
|
11
|
+
const resourceBeforeAlias = request.resource
|
|
12
|
+
const urlAfterAliasing = URL_META.applyAliases({
|
|
13
|
+
url: asFileUrl(request.pathname),
|
|
14
|
+
aliases,
|
|
15
|
+
})
|
|
16
|
+
const resourceAfterAlias = urlAfterAliasing.slice("file://".length)
|
|
17
|
+
if (resourceBeforeAlias === resourceAfterAlias) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
const resource = replaceResource(resourceBeforeAlias, resourceAfterAlias)
|
|
21
|
+
return { resource }
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const asFileUrl = (specifier) => new URL(specifier, "file:///").href
|
|
27
|
+
|
|
28
|
+
const replaceResource = (resourceBeforeAlias, newValue) => {
|
|
29
|
+
const urlObject = new URL(resourceBeforeAlias, "file://")
|
|
30
|
+
const searchSeparatorIndex = newValue.indexOf("?")
|
|
31
|
+
if (searchSeparatorIndex > -1) {
|
|
32
|
+
return newValue // let new value override search params
|
|
33
|
+
}
|
|
34
|
+
urlObject.pathname = newValue
|
|
35
|
+
const resource = `${urlObject.pathname}${urlObject.search}`
|
|
36
|
+
return resource
|
|
37
|
+
}
|
package/src/sse/sse_room.js
CHANGED
|
@@ -37,9 +37,7 @@ export const createSSERoom = ({
|
|
|
37
37
|
|
|
38
38
|
const lastKnownId =
|
|
39
39
|
request.headers["last-event-id"] ||
|
|
40
|
-
new URL(request.
|
|
41
|
-
"last-event-id",
|
|
42
|
-
)
|
|
40
|
+
new URL(request.url).searchParams.get("last-event-id")
|
|
43
41
|
|
|
44
42
|
if (clients.size >= maxClientAllowed) {
|
|
45
43
|
return {
|