@jsenv/core 24.0.2 → 24.2.2

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.
Files changed (69) hide show
  1. package/dist/jsenv_event_source_client.js +338 -0
  2. package/dist/jsenv_event_source_client.js.map +126 -0
  3. package/dist/jsenv_exploring_index.js.map +7 -7
  4. package/dist/jsenv_redirector.js +1388 -0
  5. package/dist/jsenv_redirector.js.map +384 -0
  6. package/dist/jsenv_toolbar.js +37 -504
  7. package/dist/jsenv_toolbar.js.map +37 -112
  8. package/dist/jsenv_toolbar_injector.js +31 -126
  9. package/dist/jsenv_toolbar_injector.js.map +11 -30
  10. package/{LICENSE → license} +0 -0
  11. package/package.json +2 -3
  12. package/src/buildProject.js +2 -0
  13. package/src/dev_server.js +108 -57
  14. package/src/internal/compiling/compile-directory/getOrGenerateCompiledFile.js +1 -2
  15. package/src/internal/compiling/compileFile.js +51 -23
  16. package/src/internal/compiling/createCompiledFileService.js +8 -0
  17. package/src/internal/compiling/html_source_file_service.js +43 -9
  18. package/src/internal/compiling/jsenvCompilerForHtml.js +146 -107
  19. package/src/internal/compiling/startCompileServer.js +10 -1
  20. package/src/internal/dev_server/event_source_client/event_source_client.js +63 -0
  21. package/src/internal/dev_server/event_source_client/event_source_client_file_info.js +17 -0
  22. package/src/internal/{toolbar/eventsource/connectEventSource.js → dev_server/event_source_client/event_source_connection.js} +47 -78
  23. package/src/internal/dev_server/event_source_client/file_changes.js +82 -0
  24. package/src/internal/dev_server/event_source_client/livereload_preference.js +13 -0
  25. package/src/internal/{exploring → dev_server/exploring}/exploring.css +0 -0
  26. package/src/internal/{exploring → dev_server/exploring}/exploring.html +1 -1
  27. package/src/internal/{exploring → dev_server/exploring}/exploring.js +0 -0
  28. package/src/internal/dev_server/exploring/exploring_file_info.js +21 -0
  29. package/src/internal/{exploring → dev_server/exploring}/fetchExploringJson.js +1 -1
  30. package/src/internal/{exploring/exploring.redirector.html → dev_server/redirector/redirector.html} +1 -1
  31. package/src/internal/{exploring/exploring.redirector.js → dev_server/redirector/redirector.js} +4 -2
  32. package/src/internal/dev_server/redirector/redirector_file_info.js +24 -0
  33. package/src/internal/{toolbar → dev_server/toolbar}/animation/toolbar.animation.js +0 -0
  34. package/src/internal/{toolbar → dev_server/toolbar}/backtolist/toolbar.backtolist.js +0 -0
  35. package/src/internal/{toolbar → dev_server/toolbar}/compilation/compilation.css +0 -0
  36. package/src/internal/{toolbar → dev_server/toolbar}/compilation/toolbar.compilation.js +1 -1
  37. package/src/internal/{toolbar → dev_server/toolbar}/eventsource/eventsource.css +0 -0
  38. package/src/internal/dev_server/toolbar/eventsource/toolbar.eventsource.js +83 -0
  39. package/src/internal/{toolbar → dev_server/toolbar}/execution/execution.css +0 -0
  40. package/src/internal/{toolbar → dev_server/toolbar}/execution/toolbar.execution.js +0 -0
  41. package/src/internal/{toolbar → dev_server/toolbar}/focus/focus.css +0 -0
  42. package/src/internal/{toolbar → dev_server/toolbar}/focus/toolbar.focus.js +0 -0
  43. package/src/internal/{toolbar → dev_server/toolbar}/jsenv-logo.svg +0 -0
  44. package/src/internal/{toolbar → dev_server/toolbar}/notification/toolbar.notification.js +0 -0
  45. package/src/internal/{toolbar → dev_server/toolbar}/responsive/overflow-menu.css +0 -0
  46. package/src/internal/{toolbar → dev_server/toolbar}/responsive/toolbar.responsive.js +0 -0
  47. package/src/internal/{toolbar → dev_server/toolbar}/settings/settings.css +0 -0
  48. package/src/internal/{toolbar → dev_server/toolbar}/settings/toolbar.settings.js +0 -0
  49. package/src/internal/{toolbar → dev_server/toolbar}/theme/jsenv-theme.css +0 -0
  50. package/src/internal/{toolbar → dev_server/toolbar}/theme/light-theme.css +0 -0
  51. package/src/internal/{toolbar → dev_server/toolbar}/theme/toolbar.theme.js +0 -0
  52. package/src/internal/{toolbar → dev_server/toolbar}/toolbar.html +4 -37
  53. package/src/internal/{toolbar → dev_server/toolbar}/toolbar.injector.js +3 -92
  54. package/src/internal/{toolbar → dev_server/toolbar}/toolbar.main.css +0 -0
  55. package/src/internal/{toolbar → dev_server/toolbar}/toolbar.main.js +0 -0
  56. package/src/internal/dev_server/toolbar/toolbar_file_info.js +37 -0
  57. package/src/internal/{toolbar → dev_server/toolbar}/tooltip/tooltip.css +0 -0
  58. package/src/internal/{toolbar → dev_server/toolbar}/tooltip/tooltip.js +0 -0
  59. package/src/internal/{toolbar → dev_server/toolbar}/util/animation.js +0 -0
  60. package/src/internal/{toolbar → dev_server/toolbar}/util/dom.js +0 -0
  61. package/src/internal/{toolbar → dev_server/toolbar}/util/fetching.js +2 -2
  62. package/src/internal/{toolbar → dev_server/toolbar}/util/jsenvLogger.js +0 -0
  63. package/src/internal/{toolbar → dev_server/toolbar}/util/preferences.js +0 -0
  64. package/src/internal/{toolbar → dev_server/toolbar}/util/responsive.js +0 -0
  65. package/src/internal/{toolbar → dev_server/toolbar}/util/util.js +0 -0
  66. package/src/internal/{toolbar → dev_server/toolbar}/variant/variant.js +0 -0
  67. package/src/internal/jsenvInternalFiles.js +0 -58
  68. package/src/internal/toolbar/eventsource/connectCompileServerEventSource.js +0 -74
  69. package/src/internal/toolbar/eventsource/toolbar.eventsource.js +0 -239
@@ -2,11 +2,12 @@ import { resolveUrl, urlToRelativeUrl } from "@jsenv/filesystem"
2
2
  import { moveImportMap, composeTwoImportMaps } from "@jsenv/importmap"
3
3
  import { createDetailedMessage } from "@jsenv/logger"
4
4
 
5
+ import { jsenvBrowserSystemFileInfo } from "@jsenv/core/src/internal/jsenvInternalFiles.js"
6
+ import { eventSourceClientFileInfo } from "@jsenv/core/src/internal/dev_server/event_source_client/event_source_client_file_info.js"
5
7
  import {
6
- jsenvBrowserSystemFileInfo,
7
- jsenvToolbarHtmlFileInfo,
8
- jsenvToolbarInjectorFileInfo,
9
- } from "@jsenv/core/src/internal/jsenvInternalFiles.js"
8
+ toolbarInjectorFileInfo,
9
+ toolbarHtmlFileInfo,
10
+ } from "@jsenv/core/src/internal/dev_server/toolbar/toolbar_file_info.js"
10
11
  import { fetchUrl } from "@jsenv/core/src/internal/fetchUrl.js"
11
12
  import { getDefaultImportMap } from "@jsenv/core/src/internal/import-resolution/importmap-default.js"
12
13
  import {
@@ -49,14 +50,20 @@ export const compileHtml = async ({
49
50
  sourcemapMethod,
50
51
 
51
52
  jsenvScriptInjection = true,
53
+ jsenvEventSourceClientInjection,
52
54
  jsenvToolbarInjection,
55
+ onHtmlImportmapInfo,
53
56
  }) => {
54
57
  const jsenvBrowserBuildUrlRelativeToProject = urlToRelativeUrl(
55
58
  jsenvBrowserSystemFileInfo.jsenvBuildUrl,
56
59
  projectDirectoryUrl,
57
60
  )
58
- const jsenvToolbarInjectorBuildRelativeUrlForProject = urlToRelativeUrl(
59
- jsenvToolbarInjectorFileInfo.jsenvBuildUrl,
61
+ const eventSourceClientBuildRelativeUrlForProject = urlToRelativeUrl(
62
+ eventSourceClientFileInfo.buildUrl,
63
+ projectDirectoryUrl,
64
+ )
65
+ const toolbarInjectorBuildRelativeUrlForProject = urlToRelativeUrl(
66
+ toolbarInjectorFileInfo.buildUrl,
60
67
  projectDirectoryUrl,
61
68
  )
62
69
 
@@ -67,74 +74,158 @@ export const compileHtml = async ({
67
74
  await mutateRessourceHints(htmlAst)
68
75
  }
69
76
 
77
+ const urlNoSearch = urlWithoutSearch(url)
78
+
70
79
  manipulateHtmlAst(htmlAst, {
71
80
  scriptInjections: [
72
- ...(url !== jsenvToolbarHtmlFileInfo.url && jsenvScriptInjection
81
+ ...(urlNoSearch !== toolbarHtmlFileInfo.sourceUrl && jsenvScriptInjection
73
82
  ? [
74
83
  {
75
84
  src: `/${jsenvBrowserBuildUrlRelativeToProject}`,
76
85
  },
77
86
  ]
78
87
  : []),
79
- ...(url !== jsenvToolbarHtmlFileInfo.url && jsenvToolbarInjection
88
+ ...(urlNoSearch !== toolbarHtmlFileInfo.sourceUrl &&
89
+ jsenvEventSourceClientInjection
80
90
  ? [
81
91
  {
82
- src: `/${jsenvToolbarInjectorBuildRelativeUrlForProject}`,
92
+ src: `/${eventSourceClientBuildRelativeUrlForProject}`,
93
+ },
94
+ ]
95
+ : []),
96
+ ...(urlNoSearch !== toolbarHtmlFileInfo.sourceUrl && jsenvToolbarInjection
97
+ ? [
98
+ {
99
+ src: `/${toolbarInjectorBuildRelativeUrlForProject}`,
100
+ defer: "",
101
+ async: "",
83
102
  },
84
103
  ]
85
104
  : []),
86
105
  ],
87
106
  })
88
107
 
108
+ let sources = []
109
+ let sourcesContent = []
89
110
  const { scripts } = parseHtmlAstRessources(htmlAst)
90
- const htmlDependencies = collectHtmlDependenciesFromAst(htmlAst)
91
-
92
- let hasImportmap = false
93
- const inlineScriptsContentMap = {}
94
- const importmapsToInline = []
111
+ let importmapInfo = null
95
112
  scripts.forEach((script) => {
96
113
  const typeAttribute = getHtmlNodeAttributeByName(script, "type")
97
- const srcAttribute = getHtmlNodeAttributeByName(script, "src")
98
-
99
- // importmap
100
114
  if (typeAttribute && typeAttribute.value === "importmap") {
101
- hasImportmap = true
102
-
103
- if (srcAttribute) {
104
- if (moduleOutFormat === "systemjs") {
105
- typeAttribute.value = "jsenv-importmap"
106
- return // no need to inline
115
+ if (importmapInfo) {
116
+ console.error("HTML file must contain max 1 importmap")
117
+ } else {
118
+ const srcAttribute = getHtmlNodeAttributeByName(script, "src")
119
+ const src = srcAttribute ? srcAttribute.value : ""
120
+ if (src) {
121
+ importmapInfo = {
122
+ script,
123
+ url: resolveUrl(src, url),
124
+ loadAsText: async () => {
125
+ const importMapResponse = await fetchUrl(importmapInfo.url)
126
+ if (importMapResponse.status !== 200) {
127
+ logger.warn(
128
+ createDetailedMessage(
129
+ importMapResponse.status === 404
130
+ ? `importmap script file cannot be found.`
131
+ : `importmap script file unexpected response status (${importMapResponse.status}).`,
132
+ {
133
+ "importmap url": importmapInfo.url,
134
+ "html url": url,
135
+ },
136
+ ),
137
+ )
138
+ return "{}"
139
+ }
140
+ const importmapAsText = await importMapResponse.text()
141
+ sources.push(importmapInfo.url)
142
+ sourcesContent.push(importmapAsText)
143
+
144
+ const importMapMoved = moveImportMap(
145
+ JSON.parse(importmapAsText),
146
+ importmapInfo.url,
147
+ url,
148
+ )
149
+ const compiledImportmapAsText = JSON.stringify(
150
+ importMapMoved,
151
+ null,
152
+ " ",
153
+ )
154
+ return compiledImportmapAsText
155
+ },
156
+ }
157
+ } else {
158
+ importmapInfo = {
159
+ script,
160
+ url: compiledUrl,
161
+ loadAsText: () => getHtmlNodeTextNode(script).value,
162
+ }
107
163
  }
108
-
109
- // we force inline because browsers supporting importmap supports only when they are inline
110
- importmapsToInline.push({
111
- script,
112
- src: srcAttribute.value,
113
- })
114
- return
115
- }
116
-
117
- const defaultImportMap = getDefaultImportMap({
118
- importMapFileUrl: compiledUrl,
119
- projectDirectoryUrl,
120
- compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
121
- })
122
- const inlineImportMap = JSON.parse(getHtmlNodeTextNode(script).value)
123
- const mappings = composeTwoImportMaps(defaultImportMap, inlineImportMap)
124
- if (moduleOutFormat === "systemjs") {
125
- typeAttribute.value = "jsenv-importmap"
126
164
  }
127
- setHtmlNodeText(script, JSON.stringify(mappings, null, " "))
128
- return
129
165
  }
166
+ })
167
+ if (importmapInfo) {
168
+ const htmlImportMap = JSON.parse(await importmapInfo.loadAsText())
169
+ const importMapFromJsenv = getDefaultImportMap({
170
+ importMapFileUrl: compiledUrl,
171
+ projectDirectoryUrl,
172
+ compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
173
+ })
174
+ const mappings = composeTwoImportMaps(importMapFromJsenv, htmlImportMap)
175
+ const importmapAsText = JSON.stringify(mappings, null, " ")
176
+ replaceHtmlNode(
177
+ importmapInfo.script,
178
+ `<script type="${
179
+ moduleOutFormat === "systemjs" ? "jsenv-importmap" : "importmap"
180
+ }">${importmapAsText}</script>`,
181
+ {
182
+ attributesToIgnore: ["src"],
183
+ },
184
+ )
185
+ importmapInfo.inlinedFrom = importmapInfo.url
186
+ importmapInfo.url = compiledUrl
187
+ importmapInfo.text = importmapAsText
188
+ } else {
189
+ // inject a default importmap
190
+ const defaultImportMap = getDefaultImportMap({
191
+ importMapFileUrl: compiledUrl,
192
+ projectDirectoryUrl,
193
+ compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
194
+ })
195
+ const importmapAsText = JSON.stringify(defaultImportMap, null, " ")
196
+ manipulateHtmlAst(htmlAst, {
197
+ scriptInjections: [
198
+ {
199
+ type:
200
+ moduleOutFormat === "systemjs" ? "jsenv-importmap" : "importmap",
201
+ // in case there is no importmap, force the presence
202
+ // so that '@jsenv/core/' are still remapped
203
+ text: importmapAsText,
204
+ },
205
+ ],
206
+ })
207
+ importmapInfo = {
208
+ url: compiledUrl,
209
+ text: importmapAsText,
210
+ }
211
+ }
212
+ onHtmlImportmapInfo({
213
+ htmlUrl: url,
214
+ importmapInfo,
215
+ })
130
216
 
217
+ const htmlDependencies = collectHtmlDependenciesFromAst(htmlAst)
218
+ const inlineScriptsContentMap = {}
219
+ scripts.forEach((script) => {
220
+ const typeAttribute = getHtmlNodeAttributeByName(script, "type")
221
+ const srcAttribute = getHtmlNodeAttributeByName(script, "src")
222
+ const src = srcAttribute ? srcAttribute.value : ""
131
223
  // remote module script
132
- if (typeAttribute && typeAttribute.value === "module" && srcAttribute) {
224
+ if (typeAttribute && typeAttribute.value === "module" && src) {
133
225
  if (moduleOutFormat === "systemjs") {
134
226
  removeHtmlNodeAttribute(script, typeAttribute)
135
227
  }
136
228
  removeHtmlNodeAttribute(script, srcAttribute)
137
- const src = srcAttribute.value
138
229
  const jsenvMethod =
139
230
  moduleOutFormat === "systemjs"
140
231
  ? "executeFileUsingSystemJs"
@@ -174,66 +265,6 @@ export const compileHtml = async ({
174
265
  return
175
266
  }
176
267
  })
177
-
178
- if (hasImportmap === false) {
179
- const defaultImportMap = getDefaultImportMap({
180
- importMapFileUrl: compiledUrl,
181
- projectDirectoryUrl,
182
- compileDirectoryRelativeUrl: `${outDirectoryRelativeUrl}${compileId}/`,
183
- })
184
- manipulateHtmlAst(htmlAst, {
185
- scriptInjections: [
186
- {
187
- type:
188
- moduleOutFormat === "systemjs" ? "jsenv-importmap" : "importmap",
189
- // in case there is no importmap, force the presence
190
- // so that '@jsenv/core/' are still remapped
191
- text: JSON.stringify(defaultImportMap, null, " "),
192
- },
193
- ],
194
- })
195
- }
196
-
197
- await Promise.all(
198
- importmapsToInline.map(async ({ script, src }) => {
199
- const importMapUrl = resolveUrl(src, url)
200
- const importMapResponse = await fetchUrl(importMapUrl)
201
- if (importMapResponse.status !== 200) {
202
- logger.warn(
203
- createDetailedMessage(
204
- importMapResponse.status === 404
205
- ? `Cannot inline importmap script because file cannot be found.`
206
- : `Cannot inline importmap script due to unexpected response status (${importMapResponse.status}).`,
207
- {
208
- "importmap script src": src,
209
- "importmap url": importMapUrl,
210
- "html url": url,
211
- },
212
- ),
213
- )
214
- return
215
- }
216
-
217
- const importMapContent = await importMapResponse.json()
218
- const importMapInlined = moveImportMap(
219
- importMapContent,
220
- importMapUrl,
221
- url,
222
- )
223
- replaceHtmlNode(
224
- script,
225
- `<script type="importmap">${JSON.stringify(
226
- importMapInlined,
227
- null,
228
- " ",
229
- )}</script>`,
230
- {
231
- attributesToIgnore: ["src"],
232
- },
233
- )
234
- }),
235
- )
236
-
237
268
  const htmlAfterTransformation = stringifyHtmlAst(htmlAst)
238
269
 
239
270
  let assets = []
@@ -303,12 +334,14 @@ export const compileHtml = async ({
303
334
  }
304
335
  }),
305
336
  )
337
+ sources.push(url)
338
+ sourcesContent.push(code)
306
339
 
307
340
  return {
308
341
  contentType: "text/html",
309
342
  compiledSource: htmlAfterTransformation,
310
- sources: [url],
311
- sourcesContent: [code],
343
+ sources,
344
+ sourcesContent,
312
345
  assets,
313
346
  assetsContent,
314
347
  dependencies: htmlDependencies.map(({ specifier }) => {
@@ -372,3 +405,9 @@ const mutateRessourceHints = async (htmlAst) => {
372
405
  )
373
406
  mutations.forEach((mutation) => mutation())
374
407
  }
408
+
409
+ const urlWithoutSearch = (url) => {
410
+ const urlObject = new URL(url)
411
+ urlObject.search = ""
412
+ return urlObject.href
413
+ }
@@ -104,8 +104,9 @@ export const startCompileServer = async ({
104
104
  plugins,
105
105
  livereloadSSE = false,
106
106
  transformHtmlSourceFiles = true,
107
- jsenvToolbarInjection = false,
108
107
  jsenvScriptInjection = true,
108
+ jsenvEventSourceClientInjection = false,
109
+ jsenvToolbarInjection = false,
109
110
  inlineImportMapIntoHTML = true,
110
111
  }) => {
111
112
  assertArguments({
@@ -312,6 +313,7 @@ export const startCompileServer = async ({
312
313
  customCompilers,
313
314
  moduleOutFormat,
314
315
  importMetaFormat,
316
+ jsenvEventSourceClientInjection,
315
317
  jsenvToolbarInjection,
316
318
 
317
319
  projectFileRequestedCallback,
@@ -327,8 +329,10 @@ export const startCompileServer = async ({
327
329
  createTransformHtmlSourceFileService({
328
330
  logger,
329
331
  projectDirectoryUrl,
332
+ projectFileRequestedCallback,
330
333
  inlineImportMapIntoHTML,
331
334
  jsenvScriptInjection,
335
+ jsenvEventSourceClientInjection,
332
336
  jsenvToolbarInjection,
333
337
  }),
334
338
  }
@@ -583,6 +587,9 @@ const setupServerSentEventsForLivereload = ({
583
587
  const projectFileAdded = createCallbackList()
584
588
 
585
589
  const projectFileRequestedCallback = (relativeUrl, request) => {
590
+ if (relativeUrl[0] === "/") {
591
+ relativeUrl = relativeUrl.slice(1)
592
+ }
586
593
  const url = `${projectDirectoryUrl}${relativeUrl}`
587
594
 
588
595
  if (
@@ -669,6 +676,7 @@ const setupServerSentEventsForLivereload = ({
669
676
  return
670
677
  }
671
678
 
679
+ livereloadLogger.debug(`${rootRelativeUrl} requested -> start tracking it`)
672
680
  // when a file is requested, always rebuild its dependency in case it has changed
673
681
  // since the last time it was requested
674
682
  startTrackingRoot(rootRelativeUrl)
@@ -699,6 +707,7 @@ const setupServerSentEventsForLivereload = ({
699
707
  const removeRootRemovedCallback = projectFileRemoved.add((relativeUrl) => {
700
708
  if (relativeUrl === rootRelativeUrl) {
701
709
  stopTrackingRoot(rootRelativeUrl)
710
+ livereloadLogger.debug(`${rootRelativeUrl} removed -> stop tracking it`)
702
711
  }
703
712
  })
704
713
  addStopTrackingCalback(rootRelativeUrl, removeRootRemovedCallback)
@@ -0,0 +1,63 @@
1
+ /* eslint-env browser */
2
+
3
+ import { createEventSourceConnection } from "./event_source_connection.js"
4
+ import {
5
+ getFileChanges,
6
+ addFileChange,
7
+ setFileChangeCallback,
8
+ reloadIfNeeded,
9
+ } from "./file_changes.js"
10
+ import {
11
+ isLivereloadEnabled,
12
+ setLivereloadPreference,
13
+ } from "./livereload_preference.js"
14
+
15
+ const eventsourceConnection = createEventSourceConnection(
16
+ document.location.href,
17
+ {
18
+ "file-added": ({ data }) => {
19
+ addFileChange({
20
+ file: data,
21
+ eventType: "added",
22
+ })
23
+ },
24
+ "file-modified": ({ data }) => {
25
+ addFileChange({
26
+ file: data,
27
+ eventType: "modified",
28
+ })
29
+ },
30
+ "file-removed": ({ data }) => {
31
+ addFileChange({
32
+ file: data,
33
+ eventType: "removed",
34
+ })
35
+ },
36
+ },
37
+ {
38
+ retryMaxAttempt: Infinity,
39
+ retryAllocatedMs: 20 * 1000,
40
+ },
41
+ )
42
+
43
+ const {
44
+ connect,
45
+ disconnect,
46
+ setConnectionStatusChangeCallback,
47
+ getConnectionStatus,
48
+ } = eventsourceConnection
49
+
50
+ connect()
51
+
52
+ window.__jsenv_event_source_client__ = {
53
+ connect,
54
+ disconnect,
55
+ getConnectionStatus,
56
+ setConnectionStatusChangeCallback,
57
+ getFileChanges,
58
+ addFileChange,
59
+ setFileChangeCallback,
60
+ reloadIfNeeded,
61
+ isLivereloadEnabled,
62
+ setLivereloadPreference,
63
+ }
@@ -0,0 +1,17 @@
1
+ import { jsenvCoreDirectoryUrl } from "@jsenv/core/src/internal/jsenvCoreDirectoryUrl.js"
2
+
3
+ const sourceRelativeUrl =
4
+ "./src/internal/dev_server/event_source_client/event_source_client.js"
5
+ const buildRelativeUrl = "./jsenv_event_source_client.js"
6
+ const sourceUrl = new URL(sourceRelativeUrl, jsenvCoreDirectoryUrl).href
7
+ const buildUrl = new URL(
8
+ "./dist/jsenv_event_source_client.js",
9
+ jsenvCoreDirectoryUrl,
10
+ )
11
+
12
+ export const eventSourceClientFileInfo = {
13
+ sourceRelativeUrl,
14
+ buildRelativeUrl,
15
+ sourceUrl,
16
+ buildUrl,
17
+ }
@@ -1,15 +1,9 @@
1
- export const connectEventSource = (
1
+ /* eslint-env browser */
2
+
3
+ export const createEventSourceConnection = (
2
4
  eventSourceUrl,
3
5
  events = {},
4
- {
5
- connecting = () => {},
6
- connected = () => {},
7
- cancelled = () => {},
8
- failed = () => {},
9
- retryMaxAttempt = Infinity,
10
- retryAllocatedMs = Infinity,
11
- lastEventId,
12
- } = {},
6
+ { retryMaxAttempt = Infinity, retryAllocatedMs = Infinity, lastEventId } = {},
13
7
  ) => {
14
8
  const { EventSource } = window
15
9
  if (typeof EventSource !== "function") {
@@ -18,82 +12,40 @@ export const connectEventSource = (
18
12
 
19
13
  const eventSourceOrigin = new URL(eventSourceUrl).origin
20
14
 
21
- // will be either abort, disconnect or a third function calling cancelled
22
- // depending on connectionStatus
23
- let cancelCurrentConnection = () => {}
15
+ let connectionStatus = "default"
16
+ let connectionStatusChangeCallback = () => {}
17
+ let disconnect = () => {}
24
18
 
25
- const reconnect = () => {
26
- attemptConnection(
27
- lastEventId
28
- ? addLastEventIdIntoUrlSearchParams(eventSourceUrl, lastEventId)
29
- : eventSourceUrl,
30
- )
19
+ const goToStatus = (newStatus) => {
20
+ connectionStatus = newStatus
21
+ connectionStatusChangeCallback()
31
22
  }
32
23
 
33
24
  const attemptConnection = (url) => {
34
25
  const eventSource = new EventSource(url, {
35
26
  withCredentials: true,
36
27
  })
37
-
38
- let connectionStatus = "connecting"
39
- const abort = () => {
40
- if (connectionStatus !== "connecting") {
41
- console.warn(`abort ignored because connection is ${connectionStatus}`)
28
+ disconnect = () => {
29
+ if (
30
+ connectionStatus !== "connecting" &&
31
+ connectionStatus !== "connected"
32
+ ) {
33
+ console.warn(
34
+ `disconnect() ignored because connection is ${connectionStatus}`,
35
+ )
42
36
  return
43
37
  }
44
- connectionStatus = "aborted"
45
38
  eventSource.onerror = undefined
46
39
  eventSource.close()
47
- cancelled({ connect: reconnect })
48
- }
49
- cancelCurrentConnection = abort
50
- connecting({ cancel: abort })
51
-
52
- eventSource.onopen = () => {
53
- connectionStatus = "connected"
54
- const disconnect = () => {
55
- if (connectionStatus !== "connected") {
56
- console.warn(
57
- `disconnect ignored because connection is ${connectionStatus}`,
58
- )
59
- return
60
- }
61
- connectionStatus = "disconnected"
62
- eventSource.onerror = undefined
63
- eventSource.close()
64
- cancelled({ connect: reconnect })
65
- }
66
- cancelCurrentConnection = disconnect
67
- connected({ cancel: disconnect })
40
+ goToStatus("disconnected")
68
41
  }
69
-
70
42
  let retryCount = 0
71
43
  let firstRetryMs = Date.now()
72
-
73
44
  eventSource.onerror = (errorEvent) => {
74
- const considerFailed = () => {
75
- connectionStatus = "disconnected"
76
- failed({
77
- cancel: () => {
78
- if (connectionStatus !== "failed") {
79
- console.warn(
80
- `disable ignored because connection is ${connectionStatus}`,
81
- )
82
- return
83
- }
84
- connectionStatus = "disabled"
85
- cancelled({ connect: reconnect })
86
- },
87
- connect: reconnect,
88
- })
89
- }
90
-
91
45
  if (errorEvent.target.readyState === EventSource.CONNECTING) {
92
46
  if (retryCount > retryMaxAttempt) {
93
47
  console.info(`could not connect after ${retryMaxAttempt} attempt`)
94
- eventSource.onerror = undefined
95
- eventSource.close()
96
- considerFailed()
48
+ disconnect()
97
49
  return
98
50
  }
99
51
 
@@ -105,24 +57,24 @@ export const connectEventSource = (
105
57
  console.info(
106
58
  `could not connect in less than ${retryAllocatedMs} ms`,
107
59
  )
108
- eventSource.onerror = undefined
109
- eventSource.close()
110
- considerFailed()
60
+ disconnect()
111
61
  return
112
62
  }
113
63
  }
114
64
 
115
- connectionStatus = "connecting"
116
65
  retryCount++
117
- connecting({ cancel: abort })
66
+ goToStatus("connecting")
118
67
  return
119
68
  }
120
69
 
121
70
  if (errorEvent.target.readyState === EventSource.CLOSED) {
122
- considerFailed()
71
+ disconnect()
123
72
  return
124
73
  }
125
74
  }
75
+ eventSource.onopen = () => {
76
+ goToStatus("connected")
77
+ }
126
78
  Object.keys(events).forEach((eventName) => {
127
79
  eventSource.addEventListener(eventName, (e) => {
128
80
  if (e.origin === eventSourceOrigin) {
@@ -140,21 +92,38 @@ export const connectEventSource = (
140
92
  }
141
93
  })
142
94
  }
95
+ goToStatus("connecting")
143
96
  }
144
97
 
145
- attemptConnection(eventSourceUrl)
146
- const disconnect = () => {
147
- cancelCurrentConnection()
98
+ let connect = () => {
99
+ attemptConnection(eventSourceUrl)
100
+ connect = () => {
101
+ attemptConnection(
102
+ lastEventId
103
+ ? addLastEventIdIntoUrlSearchParams(eventSourceUrl, lastEventId)
104
+ : eventSourceUrl,
105
+ )
106
+ }
148
107
  }
149
108
 
150
109
  const removePageUnloadListener = listenPageUnload(() => {
151
110
  disconnect()
152
111
  })
153
112
 
154
- return () => {
113
+ const destroy = () => {
155
114
  removePageUnloadListener()
156
115
  disconnect()
157
116
  }
117
+
118
+ return {
119
+ getConnectionStatus: () => connectionStatus,
120
+ setConnectionStatusCallback: (callback) => {
121
+ connectionStatusChangeCallback = callback
122
+ },
123
+ connect,
124
+ disconnect,
125
+ destroy,
126
+ }
158
127
  }
159
128
 
160
129
  const addLastEventIdIntoUrlSearchParams = (url, lastEventId) => {