@jsenv/server 12.8.0 → 14.0.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 +66 -81
- package/package.json +3 -2
- package/src/content_negotiation/{applyContentNegotiation.js → pick_accepted_content.js} +1 -3
- package/src/content_negotiation/{negotiateContentEncoding.js → pick_content_encoding.js} +3 -3
- package/src/content_negotiation/{negotiateContentLanguage.js → pick_content_language.js} +3 -3
- package/src/content_negotiation/{negotiateContentType.js → pick_content_type.js} +3 -3
- package/src/fetch_filesystem.js +2 -2
- package/src/internal/dns_resolution.js +17 -0
- package/src/internal/hostname_parser.js +48 -0
- package/src/internal/listen.js +15 -13
- package/src/internal/request_factory.js +3 -1
- package/src/internal/server-polyglot.js +1 -0
- package/src/internal/server_ips.js +49 -0
- package/src/main.js +11 -13
- package/src/serve_directory.js +2 -2
- package/src/service_controller.js +174 -0
- package/src/{cors/plugin_cors.js → services/cors/jsenv_service_cors.js} +29 -45
- package/src/services/error_handler/jsenv_service_error_handler.js +98 -0
- package/src/{content_negotiation/checkContentNegotiation.js → services/response_acceptance_check/jsenv_service_response_acceptance_check.js} +16 -7
- package/src/services/ressource_aliases/jsenv_service_ressource_aliases.js +40 -0
- package/src/sse/{createSSERoom.js → sse_room.js} +18 -16
- package/src/start_server.js +1045 -0
- package/src/content_negotiation/plugin_content_negotiation_check.js +0 -15
- package/src/error_to_response_default.js +0 -94
- package/src/internal/getServerOrigins.js +0 -63
- package/src/plugin_request_waiting_check.js +0 -32
- package/src/ressource_aliases/plugin_ressource_aliases.js +0 -44
- package/src/server_timing/plugin_server_timing.js +0 -28
- package/src/service_composition/service_composition.js +0 -55
- package/src/startServer.js +0 -868
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { timeStart } from "./server_timing/timing_measure.js"
|
|
2
|
+
|
|
3
|
+
const HOOK_NAMES = [
|
|
4
|
+
"serverListening",
|
|
5
|
+
"redirectRequest",
|
|
6
|
+
"handleRequest",
|
|
7
|
+
"handleWebsocket",
|
|
8
|
+
"handleError",
|
|
9
|
+
"onResponsePush",
|
|
10
|
+
"injectResponseHeaders",
|
|
11
|
+
"responseReady",
|
|
12
|
+
"serverStopped",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
export const createServiceController = (services) => {
|
|
16
|
+
const flatServices = flattenAndFilterServices(services)
|
|
17
|
+
const hookGroups = {}
|
|
18
|
+
|
|
19
|
+
const addService = (service) => {
|
|
20
|
+
Object.keys(service).forEach((key) => {
|
|
21
|
+
if (key === "name") return
|
|
22
|
+
const isHook = HOOK_NAMES.includes(key)
|
|
23
|
+
if (!isHook) {
|
|
24
|
+
console.warn(
|
|
25
|
+
`Unexpected "${key}" property on "${service.name}" service`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
const hookName = key
|
|
29
|
+
const hookValue = service[hookName]
|
|
30
|
+
if (hookValue) {
|
|
31
|
+
const group = hookGroups[hookName] || (hookGroups[hookName] = [])
|
|
32
|
+
group.push({
|
|
33
|
+
service,
|
|
34
|
+
name: hookName,
|
|
35
|
+
value: hookValue,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
flatServices.forEach((service) => {
|
|
41
|
+
addService(service)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
let currentService = null
|
|
45
|
+
let currentHookName = null
|
|
46
|
+
const callHook = (hook, info, context) => {
|
|
47
|
+
const hookFn = hook.value
|
|
48
|
+
if (!hookFn) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
currentService = hook.service
|
|
52
|
+
currentHookName = hook.name
|
|
53
|
+
let timeEnd
|
|
54
|
+
if (context && context.timing) {
|
|
55
|
+
timeEnd = timeStart(
|
|
56
|
+
`${currentService.name.replace("jsenv:", "")}.${currentHookName}`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
let valueReturned = hookFn(info, context)
|
|
60
|
+
if (context && context.timing) {
|
|
61
|
+
Object.assign(context.timing, timeEnd())
|
|
62
|
+
}
|
|
63
|
+
currentService = null
|
|
64
|
+
currentHookName = null
|
|
65
|
+
return valueReturned
|
|
66
|
+
}
|
|
67
|
+
const callAsyncHook = async (hook, info, context) => {
|
|
68
|
+
const hookFn = hook.value
|
|
69
|
+
if (!hookFn) {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
currentService = hook.service
|
|
73
|
+
currentHookName = hook.name
|
|
74
|
+
let timeEnd
|
|
75
|
+
if (context && context.timing) {
|
|
76
|
+
timeEnd = timeStart(
|
|
77
|
+
`${currentService.name.replace("jsenv:", "")}.${currentHookName}`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
let valueReturned = await hookFn(info, context)
|
|
81
|
+
if (context && context.timing) {
|
|
82
|
+
Object.assign(context.timing, timeEnd())
|
|
83
|
+
}
|
|
84
|
+
currentService = null
|
|
85
|
+
currentHookName = null
|
|
86
|
+
return valueReturned
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const callHooks = (hookName, info, context, callback = () => {}) => {
|
|
90
|
+
const hooks = hookGroups[hookName]
|
|
91
|
+
if (hooks) {
|
|
92
|
+
for (const hook of hooks) {
|
|
93
|
+
const returnValue = callHook(hook, info, context)
|
|
94
|
+
if (returnValue) {
|
|
95
|
+
callback(returnValue)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const callHooksUntil = (
|
|
101
|
+
hookName,
|
|
102
|
+
info,
|
|
103
|
+
context,
|
|
104
|
+
until = (returnValue) => returnValue,
|
|
105
|
+
) => {
|
|
106
|
+
const hooks = hookGroups[hookName]
|
|
107
|
+
if (hooks) {
|
|
108
|
+
for (const hook of hooks) {
|
|
109
|
+
const returnValue = callHook(hook, info, context)
|
|
110
|
+
const untilReturnValue = until(returnValue)
|
|
111
|
+
if (untilReturnValue) {
|
|
112
|
+
return untilReturnValue
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
const callAsyncHooksUntil = (hookName, info, context) => {
|
|
119
|
+
const hooks = hookGroups[hookName]
|
|
120
|
+
if (!hooks) {
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
if (hooks.length === 0) {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const visit = (index) => {
|
|
128
|
+
if (index >= hooks.length) {
|
|
129
|
+
return resolve()
|
|
130
|
+
}
|
|
131
|
+
const hook = hooks[index]
|
|
132
|
+
const returnValue = callAsyncHook(hook, info, context)
|
|
133
|
+
return Promise.resolve(returnValue).then((output) => {
|
|
134
|
+
if (output) {
|
|
135
|
+
return resolve(output)
|
|
136
|
+
}
|
|
137
|
+
return visit(index + 1)
|
|
138
|
+
}, reject)
|
|
139
|
+
}
|
|
140
|
+
visit(0)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
services: flatServices,
|
|
146
|
+
|
|
147
|
+
callHooks,
|
|
148
|
+
callHooksUntil,
|
|
149
|
+
callAsyncHooksUntil,
|
|
150
|
+
|
|
151
|
+
getCurrentService: () => currentService,
|
|
152
|
+
getCurrentHookName: () => currentHookName,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const flattenAndFilterServices = (services) => {
|
|
157
|
+
const flatServices = []
|
|
158
|
+
const visitServiceEntry = (serviceEntry) => {
|
|
159
|
+
if (Array.isArray(serviceEntry)) {
|
|
160
|
+
serviceEntry.forEach((value) => visitServiceEntry(value))
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (typeof serviceEntry === "object" && serviceEntry !== null) {
|
|
164
|
+
if (!serviceEntry.name) {
|
|
165
|
+
serviceEntry.name = "anonymous"
|
|
166
|
+
}
|
|
167
|
+
flatServices.push(serviceEntry)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`services must be objects, got ${serviceEntry}`)
|
|
171
|
+
}
|
|
172
|
+
services.forEach((serviceEntry) => visitServiceEntry(serviceEntry))
|
|
173
|
+
return flatServices
|
|
174
|
+
}
|
|
@@ -8,7 +8,7 @@ export const jsenvAccessControlAllowedMethods = [
|
|
|
8
8
|
"OPTIONS",
|
|
9
9
|
]
|
|
10
10
|
|
|
11
|
-
export const
|
|
11
|
+
export const jsenvServiceCORS = ({
|
|
12
12
|
accessControlAllowedOrigins = [],
|
|
13
13
|
accessControlAllowedMethods = jsenvAccessControlAllowedMethods,
|
|
14
14
|
accessControlAllowedHeaders = jsenvAccessControlAllowedHeaders,
|
|
@@ -19,7 +19,7 @@ export const pluginCORS = ({
|
|
|
19
19
|
// by default OPTIONS request can be cache for a long time, it's not going to change soon ?
|
|
20
20
|
// we could put a lot here, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
|
21
21
|
accessControlMaxAge = 600,
|
|
22
|
-
timingAllowOrigin,
|
|
22
|
+
timingAllowOrigin = false,
|
|
23
23
|
} = {}) => {
|
|
24
24
|
// TODO: we should check access control params to throw or warn if we find strange values
|
|
25
25
|
|
|
@@ -27,56 +27,40 @@ export const pluginCORS = ({
|
|
|
27
27
|
accessControlAllowRequestOrigin || accessControlAllowedOrigins.length
|
|
28
28
|
|
|
29
29
|
if (!corsEnabled) {
|
|
30
|
-
return
|
|
31
|
-
cors: {},
|
|
32
|
-
}
|
|
30
|
+
return []
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
return {
|
|
36
|
-
|
|
37
|
-
onServerParams: ({ plugins }) => {
|
|
38
|
-
if (timingAllowOrigin === undefined && plugins.server_timings) {
|
|
39
|
-
timingAllowOrigin = true
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
onRequest: (request, { shortcircuitResponse }) => {
|
|
44
|
-
if (request.method === "OPTIONS") {
|
|
45
|
-
// when request method is "OPTIONS" we must return a 200 without body
|
|
46
|
-
// So we bypass "requestToResponse" in that scenario using shortcircuitResponse
|
|
47
|
-
shortcircuitResponse({
|
|
48
|
-
status: 200,
|
|
49
|
-
headers: {
|
|
50
|
-
"content-length": 0,
|
|
51
|
-
},
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (request.parent) {
|
|
56
|
-
return null
|
|
57
|
-
}
|
|
34
|
+
name: "jsenv:cors",
|
|
58
35
|
|
|
36
|
+
handleRequest: (request) => {
|
|
37
|
+
// when request method is "OPTIONS" we must return a 200 without body
|
|
38
|
+
// So we bypass "requestToResponse" in that scenario using shortcircuitResponse
|
|
39
|
+
if (request.method === "OPTIONS") {
|
|
59
40
|
return {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
accessControlAllowedOrigins,
|
|
64
|
-
accessControlAllowRequestOrigin,
|
|
65
|
-
accessControlAllowedMethods,
|
|
66
|
-
accessControlAllowRequestMethod,
|
|
67
|
-
accessControlAllowedHeaders,
|
|
68
|
-
accessControlAllowRequestHeaders,
|
|
69
|
-
accessControlAllowCredentials,
|
|
70
|
-
accessControlMaxAge,
|
|
71
|
-
timingAllowOrigin,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
headers: accessControlHeaders,
|
|
76
|
-
}
|
|
41
|
+
status: 200,
|
|
42
|
+
headers: {
|
|
43
|
+
"content-length": 0,
|
|
77
44
|
},
|
|
78
45
|
}
|
|
79
|
-
}
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
injectResponseHeaders: (response, { request }) => {
|
|
51
|
+
const accessControlHeaders = generateAccessControlHeaders({
|
|
52
|
+
request,
|
|
53
|
+
accessControlAllowedOrigins,
|
|
54
|
+
accessControlAllowRequestOrigin,
|
|
55
|
+
accessControlAllowedMethods,
|
|
56
|
+
accessControlAllowRequestMethod,
|
|
57
|
+
accessControlAllowedHeaders,
|
|
58
|
+
accessControlAllowRequestHeaders,
|
|
59
|
+
accessControlAllowCredentials,
|
|
60
|
+
accessControlMaxAge,
|
|
61
|
+
timingAllowOrigin,
|
|
62
|
+
})
|
|
63
|
+
return accessControlHeaders
|
|
80
64
|
},
|
|
81
65
|
}
|
|
82
66
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { pickContentType } from "../../content_negotiation/pick_content_type.js"
|
|
2
|
+
|
|
3
|
+
export const jsenvServiceErrorHandler = ({ sendErrorDetails = false } = {}) => {
|
|
4
|
+
return {
|
|
5
|
+
name: "jsenv:error_handler",
|
|
6
|
+
handleError: (serverInternalError, { request }) => {
|
|
7
|
+
const serverInternalErrorIsAPrimitive =
|
|
8
|
+
serverInternalError === null ||
|
|
9
|
+
(typeof serverInternalError !== "object" &&
|
|
10
|
+
typeof serverInternalError !== "function")
|
|
11
|
+
if (!serverInternalErrorIsAPrimitive && serverInternalError.asResponse) {
|
|
12
|
+
return serverInternalError.asResponse()
|
|
13
|
+
}
|
|
14
|
+
const dataToSend = serverInternalErrorIsAPrimitive
|
|
15
|
+
? {
|
|
16
|
+
code: "VALUE_THROWED",
|
|
17
|
+
value: serverInternalError,
|
|
18
|
+
}
|
|
19
|
+
: {
|
|
20
|
+
code: serverInternalError.code || "UNKNOWN_ERROR",
|
|
21
|
+
...(sendErrorDetails
|
|
22
|
+
? {
|
|
23
|
+
stack: serverInternalError.stack,
|
|
24
|
+
...serverInternalError,
|
|
25
|
+
}
|
|
26
|
+
: {}),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const availableContentTypes = {
|
|
30
|
+
"text/html": () => {
|
|
31
|
+
const renderHtmlForErrorWithoutDetails = () => {
|
|
32
|
+
return `<p>Details not available: to enable them use jsenvServiceErrorHandler({ sendErrorDetails: true }).</p>`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const renderHtmlForErrorWithDetails = () => {
|
|
36
|
+
if (serverInternalErrorIsAPrimitive) {
|
|
37
|
+
return `<pre>${JSON.stringify(
|
|
38
|
+
serverInternalError,
|
|
39
|
+
null,
|
|
40
|
+
" ",
|
|
41
|
+
)}</pre>`
|
|
42
|
+
}
|
|
43
|
+
return `<pre>${serverInternalError.stack}</pre>`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const body = `<!DOCTYPE html>
|
|
47
|
+
<html>
|
|
48
|
+
<head>
|
|
49
|
+
<title>Internal server error</title>
|
|
50
|
+
<meta charset="utf-8" />
|
|
51
|
+
<link rel="icon" href="data:," />
|
|
52
|
+
</head>
|
|
53
|
+
|
|
54
|
+
<body>
|
|
55
|
+
<h1>Internal server error</h1>
|
|
56
|
+
<p>${
|
|
57
|
+
serverInternalErrorIsAPrimitive
|
|
58
|
+
? `Code inside server has thrown a literal.`
|
|
59
|
+
: `Code inside server has thrown an error.`
|
|
60
|
+
}</p>
|
|
61
|
+
<details>
|
|
62
|
+
<summary>See internal error details</summary>
|
|
63
|
+
${
|
|
64
|
+
sendErrorDetails
|
|
65
|
+
? renderHtmlForErrorWithDetails()
|
|
66
|
+
: renderHtmlForErrorWithoutDetails()
|
|
67
|
+
}
|
|
68
|
+
</details>
|
|
69
|
+
</body>
|
|
70
|
+
</html>`
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
headers: {
|
|
74
|
+
"content-type": "text/html",
|
|
75
|
+
"content-length": Buffer.byteLength(body),
|
|
76
|
+
},
|
|
77
|
+
body,
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"application/json": () => {
|
|
81
|
+
const body = JSON.stringify(dataToSend)
|
|
82
|
+
return {
|
|
83
|
+
headers: {
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
"content-length": Buffer.byteLength(body),
|
|
86
|
+
},
|
|
87
|
+
body,
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
const bestContentType = pickContentType(
|
|
92
|
+
request,
|
|
93
|
+
Object.keys(availableContentTypes),
|
|
94
|
+
)
|
|
95
|
+
return availableContentTypes[bestContentType || "application/json"]()
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { pickContentType } from "@jsenv/server/src/content_negotiation/pick_content_type.js"
|
|
2
|
+
import { pickContentLanguage } from "@jsenv/server/src/content_negotiation/pick_content_language.js"
|
|
3
|
+
import { pickContentEncoding } from "@jsenv/server/src/content_negotiation/pick_content_encoding.js"
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const jsenvServiceResponseAcceptanceCheck = () => {
|
|
6
|
+
return {
|
|
7
|
+
name: "jsenv:response_acceptance_check",
|
|
8
|
+
inspectResponse: (request, { response, warn }) => {
|
|
9
|
+
checkResponseAcceptance(request, response, { warn })
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const checkResponseAcceptance = (request, response, { warn }) => {
|
|
6
15
|
const requestAcceptHeader = request.headers.accept
|
|
7
16
|
const responseContentTypeHeader = response.headers["content-type"]
|
|
8
17
|
if (
|
|
9
18
|
requestAcceptHeader &&
|
|
10
19
|
responseContentTypeHeader &&
|
|
11
|
-
!
|
|
20
|
+
!pickContentType(request, [responseContentTypeHeader])
|
|
12
21
|
) {
|
|
13
22
|
warn(`response content type is not in the request accepted content types.
|
|
14
23
|
--- response content-type header ---
|
|
@@ -22,7 +31,7 @@ ${requestAcceptHeader}`)
|
|
|
22
31
|
if (
|
|
23
32
|
requestAcceptLanguageHeader &&
|
|
24
33
|
responseContentLanguageHeader &&
|
|
25
|
-
!
|
|
34
|
+
!pickContentLanguage(request, [responseContentLanguageHeader])
|
|
26
35
|
) {
|
|
27
36
|
warn(`response language is not in the request accepted language.
|
|
28
37
|
--- response content-language header ---
|
|
@@ -36,7 +45,7 @@ ${requestAcceptLanguageHeader}`)
|
|
|
36
45
|
if (
|
|
37
46
|
requestAcceptLanguageHeader &&
|
|
38
47
|
responseContentLanguageHeader &&
|
|
39
|
-
!
|
|
48
|
+
!pickContentEncoding(request, [responseContentLanguageHeader])
|
|
40
49
|
) {
|
|
41
50
|
warn(`response encoding is not in the request accepted encoding.
|
|
42
51
|
--- response content-encoding header ---
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { URL_META } from "@jsenv/url-meta"
|
|
2
|
+
|
|
3
|
+
export const jsenvServiceRessourceAliases = (ressourceAliases) => {
|
|
4
|
+
const aliases = {}
|
|
5
|
+
Object.keys(ressourceAliases).forEach((key) => {
|
|
6
|
+
aliases[asFileUrl(key)] = asFileUrl(ressourceAliases[key])
|
|
7
|
+
})
|
|
8
|
+
return {
|
|
9
|
+
name: "jsenv:ressource_aliases",
|
|
10
|
+
redirectRequest: (request) => {
|
|
11
|
+
const ressourceBeforeAlias = request.ressource
|
|
12
|
+
const urlAfterAliasing = URL_META.applyAliases({
|
|
13
|
+
url: asFileUrl(request.pathname),
|
|
14
|
+
aliases,
|
|
15
|
+
})
|
|
16
|
+
const ressourceAfterAlias = urlAfterAliasing.slice("file://".length)
|
|
17
|
+
if (ressourceBeforeAlias === ressourceAfterAlias) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
const ressource = replaceRessource(
|
|
21
|
+
ressourceBeforeAlias,
|
|
22
|
+
ressourceAfterAlias,
|
|
23
|
+
)
|
|
24
|
+
return { ressource }
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const asFileUrl = (specifier) => new URL(specifier, "file:///").href
|
|
30
|
+
|
|
31
|
+
const replaceRessource = (ressourceBeforeAlias, newValue) => {
|
|
32
|
+
const urlObject = new URL(ressourceBeforeAlias, "file://")
|
|
33
|
+
const searchSeparatorIndex = newValue.indexOf("?")
|
|
34
|
+
if (searchSeparatorIndex > -1) {
|
|
35
|
+
return newValue // let new value override search params
|
|
36
|
+
}
|
|
37
|
+
urlObject.pathname = newValue
|
|
38
|
+
const ressource = `${urlObject.pathname}${urlObject.search}`
|
|
39
|
+
return ressource
|
|
40
|
+
}
|
|
@@ -57,6 +57,7 @@ export const createSSERoom = ({
|
|
|
57
57
|
const client = {
|
|
58
58
|
next,
|
|
59
59
|
complete,
|
|
60
|
+
request,
|
|
60
61
|
}
|
|
61
62
|
if (clients.size === 0) {
|
|
62
63
|
const effectReturnValue = effect()
|
|
@@ -94,7 +95,9 @@ export const createSSERoom = ({
|
|
|
94
95
|
|
|
95
96
|
// send to everyone
|
|
96
97
|
if (welcomeEventPublic) {
|
|
97
|
-
|
|
98
|
+
sendEventToAllClients(welcomeEvent, {
|
|
99
|
+
history: false,
|
|
100
|
+
})
|
|
98
101
|
}
|
|
99
102
|
// send only to this client
|
|
100
103
|
else {
|
|
@@ -148,14 +151,17 @@ export const createSSERoom = ({
|
|
|
148
151
|
eventHistory.add(event)
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
154
|
+
const sendEventToAllClients = (event, { history = true } = {}) => {
|
|
155
|
+
if (history) {
|
|
153
156
|
addEventToHistory(event)
|
|
154
157
|
}
|
|
155
158
|
logger.debug(
|
|
156
159
|
`send "${event.type}" event to ${clients.size} client in the room`,
|
|
157
160
|
)
|
|
158
|
-
|
|
161
|
+
const eventString = stringifySourceEvent(event)
|
|
162
|
+
clients.forEach((client) => {
|
|
163
|
+
client.next(eventString)
|
|
164
|
+
})
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
const getAllEventSince = (id) => {
|
|
@@ -166,21 +172,18 @@ export const createSSERoom = ({
|
|
|
166
172
|
return events
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
const write = (data) => {
|
|
170
|
-
clients.forEach((client) => {
|
|
171
|
-
client.next(data)
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
|
|
175
175
|
const keepAlive = () => {
|
|
176
176
|
// maybe that, when an event occurs, we can delay the keep alive event
|
|
177
177
|
logger.debug(
|
|
178
178
|
`send keep alive event, number of client listening event source: ${clients.size}`,
|
|
179
179
|
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
sendEventToAllClients(
|
|
181
|
+
{
|
|
182
|
+
type: "comment",
|
|
183
|
+
data: new Date().toLocaleTimeString(),
|
|
184
|
+
},
|
|
185
|
+
{ history: false },
|
|
186
|
+
)
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
const open = () => {
|
|
@@ -209,7 +212,7 @@ export const createSSERoom = ({
|
|
|
209
212
|
// - ability to sendEvent to clients in the room
|
|
210
213
|
// - ability to join the room
|
|
211
214
|
// - ability to leave the room
|
|
212
|
-
|
|
215
|
+
sendEventToAllClients,
|
|
213
216
|
join,
|
|
214
217
|
leave,
|
|
215
218
|
|
|
@@ -256,7 +259,6 @@ const disconnectRequestFromRoom = (request, room) => {
|
|
|
256
259
|
if (!requestInfo) {
|
|
257
260
|
return
|
|
258
261
|
}
|
|
259
|
-
|
|
260
262
|
const { sseProducer, roomObservableMap } = requestInfo
|
|
261
263
|
const roomObservable = roomObservableMap.get(room)
|
|
262
264
|
roomObservableMap.delete(room)
|