@jsenv/core 27.4.0 → 27.5.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 (86) hide show
  1. package/dist/js/autoreload.js +359 -0
  2. package/dist/js/execute_using_dynamic_import.js +1 -1
  3. package/dist/js/html_supervisor_installer.js +469 -254
  4. package/dist/js/html_supervisor_setup.js +3 -4
  5. package/dist/js/new_stylesheet.js +26 -58
  6. package/dist/js/server_events_client.js +307 -0
  7. package/dist/main.js +7558 -7311
  8. package/package.json +12 -10
  9. package/{README.md → readme.md} +8 -9
  10. package/src/build/build.js +12 -16
  11. package/src/build/start_build_server.js +24 -28
  12. package/src/dev/start_dev_server.js +34 -96
  13. package/src/execute/execute.js +17 -35
  14. package/src/omega/errors.js +20 -18
  15. package/src/omega/kitchen.js +7 -6
  16. package/src/omega/omega_server.js +96 -127
  17. package/src/omega/server/file_service.js +247 -46
  18. package/src/omega/url_graph.js +33 -20
  19. package/src/plugins/autoreload/client/autoreload.js +201 -0
  20. package/src/plugins/autoreload/{dev_sse/client → client}/autoreload_preference.js +0 -0
  21. package/src/plugins/autoreload/{dev_sse/client → client}/reload.js +29 -10
  22. package/src/plugins/autoreload/{dev_sse/client → client}/url_helpers.js +0 -0
  23. package/src/plugins/autoreload/jsenv_plugin_autoreload.js +4 -4
  24. package/src/plugins/autoreload/{dev_sse/jsenv_plugin_dev_sse_client.js → jsenv_plugin_autoreload_client.js} +8 -8
  25. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +196 -0
  26. package/src/{dev/plugins → plugins}/explorer/client/explorer.html +0 -0
  27. package/src/{dev/plugins → plugins}/explorer/client/jsenv.png +0 -0
  28. package/src/{dev/plugins → plugins}/explorer/jsenv_plugin_explorer.js +1 -3
  29. package/src/plugins/html_supervisor/client/error_formatter.js +300 -0
  30. package/src/plugins/html_supervisor/client/error_overlay.js +172 -0
  31. package/src/plugins/html_supervisor/client/html_supervisor_installer.js +124 -54
  32. package/src/plugins/html_supervisor/client/html_supervisor_setup.js +3 -4
  33. package/src/plugins/html_supervisor/jsenv_plugin_html_supervisor.js +72 -27
  34. package/src/plugins/inline/jsenv_plugin_html_inline_content.js +97 -117
  35. package/src/plugins/node_esm_resolution/jsenv_plugin_node_esm_resolution.js +66 -58
  36. package/src/plugins/plugin_controller.js +102 -67
  37. package/src/plugins/plugins.js +10 -8
  38. package/src/{helpers/event_source/event_source.js → plugins/server_events/client/event_source_connection.js} +102 -31
  39. package/src/plugins/server_events/client/server_events_client.js +17 -0
  40. package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +48 -0
  41. package/src/plugins/server_events/server_events_dispatcher.js +69 -0
  42. package/src/{dev/plugins → plugins}/toolbar/client/animation/toolbar_animation.js +0 -0
  43. package/src/{dev/plugins → plugins}/toolbar/client/eventsource/eventsource.css +0 -0
  44. package/src/{dev/plugins → plugins}/toolbar/client/eventsource/toolbar_eventsource.js +0 -0
  45. package/src/{dev/plugins → plugins}/toolbar/client/execution/execution.css +0 -0
  46. package/src/{dev/plugins → plugins}/toolbar/client/execution/toolbar_execution.js +0 -0
  47. package/src/{dev/plugins → plugins}/toolbar/client/focus/focus.css +0 -0
  48. package/src/{dev/plugins → plugins}/toolbar/client/focus/toolbar_focus.js +0 -0
  49. package/src/{dev/plugins → plugins}/toolbar/client/jsenv_logo.svg +0 -0
  50. package/src/{dev/plugins → plugins}/toolbar/client/notification/toolbar_notification.js +0 -0
  51. package/src/{dev/plugins → plugins}/toolbar/client/responsive/overflow_menu.css +0 -0
  52. package/src/{dev/plugins → plugins}/toolbar/client/responsive/toolbar_responsive.js +0 -0
  53. package/src/{dev/plugins → plugins}/toolbar/client/settings/settings.css +0 -0
  54. package/src/{dev/plugins → plugins}/toolbar/client/settings/toolbar_settings.js +0 -0
  55. package/src/{dev/plugins → plugins}/toolbar/client/theme/jsenv_theme.css +0 -0
  56. package/src/{dev/plugins → plugins}/toolbar/client/theme/light_theme.css +0 -0
  57. package/src/{dev/plugins → plugins}/toolbar/client/theme/toolbar_theme.js +0 -0
  58. package/src/{dev/plugins → plugins}/toolbar/client/toolbar.html +0 -0
  59. package/src/{dev/plugins → plugins}/toolbar/client/toolbar_injector.js +0 -0
  60. package/src/{dev/plugins → plugins}/toolbar/client/toolbar_main.css +0 -0
  61. package/src/{dev/plugins → plugins}/toolbar/client/toolbar_main.js +0 -0
  62. package/src/{dev/plugins → plugins}/toolbar/client/tooltip/tooltip.css +0 -0
  63. package/src/{dev/plugins → plugins}/toolbar/client/tooltip/tooltip.js +0 -0
  64. package/src/{dev/plugins → plugins}/toolbar/client/util/animation.js +0 -0
  65. package/src/{dev/plugins → plugins}/toolbar/client/util/dom.js +0 -0
  66. package/src/{dev/plugins → plugins}/toolbar/client/util/fetch_using_xhr.js +0 -0
  67. package/src/{dev/plugins → plugins}/toolbar/client/util/fetching.js +0 -0
  68. package/src/{dev/plugins → plugins}/toolbar/client/util/iframe_to_parent_href.js +0 -0
  69. package/src/{dev/plugins → plugins}/toolbar/client/util/jsenv_logger.js +0 -0
  70. package/src/{dev/plugins → plugins}/toolbar/client/util/preferences.js +0 -0
  71. package/src/{dev/plugins → plugins}/toolbar/client/util/responsive.js +0 -0
  72. package/src/{dev/plugins → plugins}/toolbar/client/util/util.js +0 -0
  73. package/src/{dev/plugins → plugins}/toolbar/client/variant/variant.js +0 -0
  74. package/src/{dev/plugins → plugins}/toolbar/jsenv_plugin_toolbar.js +0 -0
  75. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_html.js +4 -3
  76. package/src/plugins/transpilation/babel/new_stylesheet/client/new_stylesheet.js +25 -55
  77. package/src/plugins/transpilation/import_assertions/jsenv_plugin_import_assertions.js +44 -24
  78. package/src/plugins/transpilation/jsenv_plugin_transpilation.js +6 -1
  79. package/src/plugins/url_analysis/html/html_urls.js +8 -8
  80. package/src/test/execute_plan.js +36 -54
  81. package/src/test/execute_test_plan.js +2 -2
  82. package/dist/js/event_source_client.js +0 -549
  83. package/src/helpers/event_source/sse_service.js +0 -53
  84. package/src/plugins/autoreload/dev_sse/client/event_source_client.js +0 -193
  85. package/src/plugins/autoreload/dev_sse/jsenv_plugin_dev_sse_server.js +0 -192
  86. package/src/plugins/html_supervisor/client/error_in_document.js +0 -345
@@ -1,44 +1,63 @@
1
1
  import { timeStart } from "@jsenv/server"
2
2
 
3
- export const createPluginController = ({
4
- plugins,
5
- scenario,
6
- hooks = [
7
- "init",
8
- "resolveUrl",
9
- "redirectUrl",
10
- "fetchUrlContent",
11
- "transformUrlContent",
12
- "transformUrlSearchParams",
13
- "formatUrl",
14
- "finalizeUrlContent",
15
- "cooked",
16
- "destroy",
17
- ],
18
- }) => {
19
- plugins = flattenAndFilterPlugins(plugins, { scenario })
20
- // precompute a list of hooks per hookName
21
- // For one major reason:
3
+ const HOOK_NAMES = [
4
+ "init",
5
+ "serve", // is called only during dev/tests
6
+ "resolveUrl",
7
+ "redirectUrl",
8
+ "fetchUrlContent",
9
+ "transformUrlContent",
10
+ "transformUrlSearchParams",
11
+ "formatUrl",
12
+ "finalizeUrlContent",
13
+ "bundle", // is called only during build
14
+ "optimizeUrlContent", // is called only during build
15
+ "cooked",
16
+ "augmentResponse", // is called only during dev/tests
17
+ "destroy",
18
+ ]
19
+
20
+ export const createPluginController = ({ plugins, scenario }) => {
21
+ const flatPlugins = flattenAndFilterPlugins(plugins, { scenario })
22
+ // precompute a list of hooks per hookName for one major reason:
22
23
  // - When debugging, there is less iteration
23
- // And also it should increase perf as there is less work to do
24
+ // also it should increase perf as there is less work to do
25
+
24
26
  const hookGroups = {}
25
- const addHook = (hookName) => {
26
- const hooks = []
27
- plugins.forEach((plugin) => {
28
- const hook = plugin[hookName]
29
- if (hook) {
30
- hooks.push({
27
+ const addPlugin = (plugin, { position = "start" }) => {
28
+ Object.keys(plugin).forEach((key) => {
29
+ if (key === "name" || key === "appliesDuring" || key === "serverEvents") {
30
+ return
31
+ }
32
+ const isHook = HOOK_NAMES.includes(key)
33
+ if (!isHook) {
34
+ console.warn(`Unexpected "${key}" property on "${plugin.name}" plugin`)
35
+ }
36
+ const hookName = key
37
+ const hookValue = plugin[hookName]
38
+ if (hookValue) {
39
+ const group = hookGroups[hookName] || (hookGroups[hookName] = [])
40
+ const hook = {
31
41
  plugin,
32
- hookName,
33
- value: hook,
34
- })
42
+ name: hookName,
43
+ value: hookValue,
44
+ }
45
+ if (position === "start") {
46
+ group.push(hook)
47
+ } else {
48
+ group.unshift(hook)
49
+ }
35
50
  }
36
51
  })
37
- hookGroups[hookName] = hooks
38
- return hooks
39
52
  }
40
- hooks.forEach((hookName) => {
41
- addHook(hookName)
53
+ const pushPlugin = (plugin) => {
54
+ addPlugin(plugin, { position: "start" })
55
+ }
56
+ const unshiftPlugin = (plugin) => {
57
+ addPlugin(plugin, { position: "end" })
58
+ }
59
+ flatPlugins.forEach((plugin) => {
60
+ pushPlugin(plugin)
42
61
  })
43
62
 
44
63
  let currentPlugin = null
@@ -49,15 +68,18 @@ export const createPluginController = ({
49
68
  return null
50
69
  }
51
70
  currentPlugin = hook.plugin
52
- currentHookName = hook.hookName
53
- const timeEnd = timeStart(
54
- `${currentHookName}-${currentPlugin.name.replace("jsenv:", "")}`,
55
- )
71
+ currentHookName = hook.name
72
+ let timeEnd
73
+ if (info.timing) {
74
+ timeEnd = timeStart(
75
+ `${currentHookName}-${currentPlugin.name.replace("jsenv:", "")}`,
76
+ )
77
+ }
56
78
  let valueReturned = hookFn(info, context)
57
79
  if (info.timing) {
58
80
  Object.assign(info.timing, timeEnd())
59
81
  }
60
- valueReturned = assertAndNormalizeReturnValue(hook.hookName, valueReturned)
82
+ valueReturned = assertAndNormalizeReturnValue(hook.name, valueReturned)
61
83
  currentPlugin = null
62
84
  currentHookName = null
63
85
  return valueReturned
@@ -68,15 +90,18 @@ export const createPluginController = ({
68
90
  return null
69
91
  }
70
92
  currentPlugin = hook.plugin
71
- currentHookName = hook.hookName
72
- const timeEnd = timeStart(
73
- `${currentHookName}-${currentPlugin.name.replace("jsenv:", "")}`,
74
- )
93
+ currentHookName = hook.name
94
+ let timeEnd
95
+ if (info.timing) {
96
+ timeEnd = timeStart(
97
+ `${currentHookName}-${currentPlugin.name.replace("jsenv:", "")}`,
98
+ )
99
+ }
75
100
  let valueReturned = await hookFn(info, context)
76
101
  if (info.timing) {
77
102
  Object.assign(info.timing, timeEnd())
78
103
  }
79
- valueReturned = assertAndNormalizeReturnValue(hook.hookName, valueReturned)
104
+ valueReturned = assertAndNormalizeReturnValue(hook.name, valueReturned)
80
105
  currentPlugin = null
81
106
  currentHookName = null
82
107
  return valueReturned
@@ -84,36 +109,45 @@ export const createPluginController = ({
84
109
 
85
110
  const callHooks = (hookName, info, context, callback) => {
86
111
  const hooks = hookGroups[hookName]
87
- for (const hook of hooks) {
88
- const returnValue = callHook(hook, info, context)
89
- if (returnValue) {
90
- callback(returnValue)
112
+ if (hooks) {
113
+ for (const hook of hooks) {
114
+ const returnValue = callHook(hook, info, context)
115
+ if (returnValue && callback) {
116
+ callback(returnValue)
117
+ }
91
118
  }
92
119
  }
93
120
  }
94
121
  const callAsyncHooks = async (hookName, info, context, callback) => {
95
122
  const hooks = hookGroups[hookName]
96
- await hooks.reduce(async (previous, hook) => {
97
- await previous
98
- const returnValue = await callAsyncHook(hook, info, context)
99
- if (returnValue && callback) {
100
- await callback(returnValue)
101
- }
102
- }, Promise.resolve())
123
+ if (hooks) {
124
+ await hooks.reduce(async (previous, hook) => {
125
+ await previous
126
+ const returnValue = await callAsyncHook(hook, info, context)
127
+ if (returnValue && callback) {
128
+ await callback(returnValue)
129
+ }
130
+ }, Promise.resolve())
131
+ }
103
132
  }
104
133
 
105
134
  const callHooksUntil = (hookName, info, context) => {
106
135
  const hooks = hookGroups[hookName]
107
- for (const hook of hooks) {
108
- const returnValue = callHook(hook, info, context)
109
- if (returnValue) {
110
- return returnValue
136
+ if (hooks) {
137
+ for (const hook of hooks) {
138
+ const returnValue = callHook(hook, info, context)
139
+ if (returnValue) {
140
+ return returnValue
141
+ }
111
142
  }
112
143
  }
113
144
  return null
114
145
  }
115
146
  const callAsyncHooksUntil = (hookName, info, context) => {
116
147
  const hooks = hookGroups[hookName]
148
+ if (!hooks) {
149
+ return null
150
+ }
117
151
  if (hooks.length === 0) {
118
152
  return null
119
153
  }
@@ -136,8 +170,9 @@ export const createPluginController = ({
136
170
  }
137
171
 
138
172
  return {
139
- plugins,
140
- addHook,
173
+ plugins: flatPlugins,
174
+ pushPlugin,
175
+ unshiftPlugin,
141
176
  getHookFunction,
142
177
  callHook,
143
178
  callAsyncHook,
@@ -152,8 +187,8 @@ export const createPluginController = ({
152
187
  }
153
188
  }
154
189
 
155
- const flattenAndFilterPlugins = (pluginsRaw, { scenario }) => {
156
- const plugins = []
190
+ const flattenAndFilterPlugins = (plugins, { scenario }) => {
191
+ const flatPlugins = []
157
192
  const visitPluginEntry = (pluginEntry) => {
158
193
  if (Array.isArray(pluginEntry)) {
159
194
  pluginEntry.forEach((value) => visitPluginEntry(value))
@@ -169,7 +204,7 @@ const flattenAndFilterPlugins = (pluginsRaw, { scenario }) => {
169
204
  return
170
205
  }
171
206
  if (appliesDuring === "*") {
172
- plugins.push(pluginEntry)
207
+ flatPlugins.push(pluginEntry)
173
208
  return
174
209
  }
175
210
  if (typeof appliesDuring === "string") {
@@ -179,7 +214,7 @@ const flattenAndFilterPlugins = (pluginsRaw, { scenario }) => {
179
214
  )
180
215
  }
181
216
  if (appliesDuring === scenario) {
182
- plugins.push(pluginEntry)
217
+ flatPlugins.push(pluginEntry)
183
218
  }
184
219
  return
185
220
  }
@@ -189,7 +224,7 @@ const flattenAndFilterPlugins = (pluginsRaw, { scenario }) => {
189
224
  )
190
225
  }
191
226
  if (appliesDuring[scenario]) {
192
- plugins.push(pluginEntry)
227
+ flatPlugins.push(pluginEntry)
193
228
  return
194
229
  }
195
230
  if (pluginEntry.destroy) {
@@ -199,8 +234,8 @@ const flattenAndFilterPlugins = (pluginsRaw, { scenario }) => {
199
234
  }
200
235
  throw new Error(`plugin must be objects, got ${pluginEntry}`)
201
236
  }
202
- pluginsRaw.forEach((plugin) => visitPluginEntry(plugin))
203
- return plugins
237
+ plugins.forEach((plugin) => visitPluginEntry(plugin))
238
+ return flatPlugins
204
239
  }
205
240
 
206
241
  const getHookFunction = (
@@ -19,16 +19,17 @@ import { jsenvPluginMinification } from "./minification/jsenv_plugin_minificatio
19
19
  import { jsenvPluginImportMetaHot } from "./import_meta_hot/jsenv_plugin_import_meta_hot.js"
20
20
  import { jsenvPluginAutoreload } from "./autoreload/jsenv_plugin_autoreload.js"
21
21
  import { jsenvPluginCacheControl } from "./cache_control/jsenv_plugin_cache_control.js"
22
+ // dev only
23
+ import { jsenvPluginExplorer } from "./explorer/jsenv_plugin_explorer.js"
22
24
 
23
25
  export const getCorePlugins = ({
24
26
  rootDirectoryUrl,
25
- urlGraph,
26
27
  scenario,
27
28
  runtimeCompat,
28
29
 
29
30
  urlAnalysis = {},
30
31
  htmlSupervisor,
31
- nodeEsmResolution,
32
+ nodeEsmResolution = true,
32
33
  fileSystemMagicResolution,
33
34
  directoryReferenceAllowed,
34
35
  transpilation = true,
@@ -38,6 +39,7 @@ export const getCorePlugins = ({
38
39
  clientAutoreload = false,
39
40
  clientFileChangeCallbackList,
40
41
  clientFilesPruneCallbackList,
42
+ explorer,
41
43
  } = {}) => {
42
44
  if (htmlSupervisor === true) {
43
45
  htmlSupervisor = {}
@@ -45,9 +47,13 @@ export const getCorePlugins = ({
45
47
  if (nodeEsmResolution === true) {
46
48
  nodeEsmResolution = {}
47
49
  }
50
+ if (fileSystemMagicResolution === true) {
51
+ fileSystemMagicResolution = {}
52
+ }
48
53
  if (clientAutoreload === true) {
49
54
  clientAutoreload = {}
50
55
  }
56
+
51
57
  return [
52
58
  jsenvPluginUrlAnalysis({ rootDirectoryUrl, ...urlAnalysis }),
53
59
  jsenvPluginTranspilation(transpilation),
@@ -63,12 +69,7 @@ export const getCorePlugins = ({
63
69
  jsenvPluginHttpUrls(),
64
70
  jsenvPluginLeadingSlash(),
65
71
  // before url resolution to handle "js_import_export" resolution
66
- jsenvPluginNodeEsmResolution({
67
- rootDirectoryUrl,
68
- urlGraph,
69
- runtimeCompat,
70
- ...nodeEsmResolution,
71
- }),
72
+ jsenvPluginNodeEsmResolution(nodeEsmResolution),
72
73
  jsenvPluginUrlResolution(),
73
74
  jsenvPluginUrlVersion(),
74
75
  jsenvPluginCommonJsGlobals(),
@@ -90,5 +91,6 @@ export const getCorePlugins = ({
90
91
  ]
91
92
  : []),
92
93
  jsenvPluginCacheControl(),
94
+ ...(explorer ? [jsenvPluginExplorer(explorer)] : []),
93
95
  ]
94
96
  }
@@ -1,5 +1,3 @@
1
- /* eslint-env browser */
2
-
3
1
  const STATUSES = {
4
2
  CONNECTING: "connecting",
5
3
  CONNECTED: "connected",
@@ -8,7 +6,12 @@ const STATUSES = {
8
6
 
9
7
  export const createEventSourceConnection = (
10
8
  eventSourceUrl,
11
- { retryMaxAttempt = Infinity, retryAllocatedMs = Infinity, lastEventId } = {},
9
+ {
10
+ retryMaxAttempt = Infinity,
11
+ retryAllocatedMs = Infinity,
12
+ lastEventId,
13
+ useEventsToManageConnection = true,
14
+ } = {},
12
15
  ) => {
13
16
  const { EventSource } = window
14
17
  if (typeof EventSource !== "function") {
@@ -16,25 +19,85 @@ export const createEventSourceConnection = (
16
19
  }
17
20
 
18
21
  let eventSource
19
- const events = {}
22
+ const listenersMap = new Map()
23
+ const callbacksMap = new Map()
20
24
  const eventSourceOrigin = new URL(eventSourceUrl).origin
21
- const addEventCallbacks = (eventCallbacks) => {
22
- Object.keys(eventCallbacks).forEach((eventName) => {
23
- const eventCallback = eventCallbacks[eventName]
24
- events[eventName] = (e) => {
25
- if (e.origin === eventSourceOrigin) {
26
- if (e.lastEventId) {
27
- lastEventId = e.lastEventId
25
+ const addEventCallbacks = (namedCallbacks) => {
26
+ let listenersMapSize = listenersMap.size
27
+ Object.keys(namedCallbacks).forEach((eventName) => {
28
+ const callback = namedCallbacks[eventName]
29
+ const existingCallbacks = callbacksMap.get(eventName)
30
+ let callbacks
31
+ if (existingCallbacks) {
32
+ callbacks = existingCallbacks
33
+ } else {
34
+ callbacks = []
35
+ callbacksMap.set(eventName, callbacks)
36
+ }
37
+ if (callbacks.length === 0) {
38
+ const eventListener = (e) => {
39
+ if (e.origin === eventSourceOrigin) {
40
+ if (e.lastEventId) {
41
+ lastEventId = e.lastEventId
42
+ }
43
+ callbacks.forEach((eventCallback) => {
44
+ eventCallback(e)
45
+ })
28
46
  }
29
- eventCallback(e)
47
+ }
48
+ listenersMap.set(eventName, eventListener)
49
+ if (eventSource) {
50
+ eventSource.addEventListener(eventName, eventListener)
30
51
  }
31
52
  }
32
- if (eventSource) {
33
- eventSource.addEventListener(eventName, events[eventName])
34
- }
53
+ callbacks.push(callback)
35
54
  })
55
+ if (
56
+ useEventsToManageConnection &&
57
+ listenersMapSize === 0 &&
58
+ listenersMap.size > 0 &&
59
+ status.value !== STATUSES.CONNECTING &&
60
+ status.value !== STATUSES.CONNECTED
61
+ ) {
62
+ _connect()
63
+ }
64
+
65
+ let removed = false
66
+ return () => {
67
+ if (removed) return
68
+ removed = true
69
+ listenersMapSize = listenersMap.size
70
+ Object.keys(namedCallbacks).forEach((eventName) => {
71
+ const callback = namedCallbacks[eventName]
72
+ const callbacks = callbacksMap.get(eventName)
73
+ if (callbacks) {
74
+ const index = callbacks.indexOf(callback)
75
+ if (index > -1) {
76
+ callbacks.splice(index, 1)
77
+ if (callbacks.length === 0) {
78
+ const listener = listenersMap.get(eventName)
79
+ if (listener) {
80
+ listenersMap.delete(listener)
81
+ if (eventSource) {
82
+ eventSource.removeEventListener(eventName, listener)
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ })
89
+ namedCallbacks = null // allow garbage collect
90
+ if (
91
+ useEventsToManageConnection &&
92
+ listenersMapSize > 0 &&
93
+ listenersMap.size === 0 &&
94
+ (status.value === STATUSES.CONNECTING ||
95
+ status.value === STATUSES.CONNECTED)
96
+ ) {
97
+ _disconnect()
98
+ }
99
+ }
36
100
  }
37
- addEventCallbacks(events)
38
101
 
39
102
  const status = {
40
103
  value: "default",
@@ -50,6 +113,12 @@ export const createEventSourceConnection = (
50
113
  let _disconnect = () => {}
51
114
 
52
115
  const attemptConnection = (url) => {
116
+ if (
117
+ status.value === STATUSES.CONNECTING ||
118
+ status.value === STATUSES.CONNECTED
119
+ ) {
120
+ return
121
+ }
53
122
  eventSource = new EventSource(url, {
54
123
  withCredentials: true,
55
124
  })
@@ -63,11 +132,13 @@ export const createEventSourceConnection = (
63
132
  )
64
133
  return
65
134
  }
66
- eventSource.onerror = undefined
67
- eventSource.close()
68
- Object.keys(events).forEach((eventName) => {
69
- eventSource.removeEventListener(eventName, events[eventName])
70
- })
135
+ if (eventSource) {
136
+ eventSource.onerror = undefined
137
+ eventSource.close()
138
+ listenersMap.forEach((listener, eventName) => {
139
+ eventSource.removeEventListener(eventName, listener)
140
+ })
141
+ }
71
142
  eventSource = null
72
143
  status.goTo(STATUSES.DISCONNECTED)
73
144
  }
@@ -107,22 +178,20 @@ export const createEventSourceConnection = (
107
178
  eventSource.onopen = () => {
108
179
  status.goTo(STATUSES.CONNECTED)
109
180
  }
110
- Object.keys(events).forEach((eventName) => {
111
- eventSource.addEventListener(eventName, events[eventName])
181
+ listenersMap.forEach((listener, eventName) => {
182
+ eventSource.addEventListener(eventName, listener)
112
183
  })
113
- if (!events.hasOwnProperty("welcome")) {
114
- eventSource.addEventListener("welcome", (e) => {
115
- if (e.origin === eventSourceOrigin && e.lastEventId) {
116
- lastEventId = e.lastEventId
117
- }
184
+ if (!listenersMap.has("welcome")) {
185
+ addEventCallbacks({
186
+ welcome: () => {}, // to update lastEventId
118
187
  })
119
188
  }
120
189
  status.goTo(STATUSES.CONNECTING)
121
190
  }
122
191
 
123
- let connect = () => {
192
+ let _connect = () => {
124
193
  attemptConnection(eventSourceUrl)
125
- connect = () => {
194
+ _connect = () => {
126
195
  attemptConnection(
127
196
  lastEventId
128
197
  ? addLastEventIdIntoUrlSearchParams(eventSourceUrl, lastEventId)
@@ -143,11 +212,13 @@ export const createEventSourceConnection = (
143
212
  const destroy = () => {
144
213
  removePageUnloadListener()
145
214
  _disconnect()
215
+ listenersMap.clear()
216
+ callbacksMap.clear()
146
217
  }
147
218
 
148
219
  return {
149
220
  status,
150
- connect,
221
+ connect: () => _connect(),
151
222
  addEventCallbacks,
152
223
  disconnect: () => _disconnect(),
153
224
  destroy,
@@ -0,0 +1,17 @@
1
+ import { createEventSourceConnection } from "./event_source_connection.js"
2
+
3
+ const eventsourceConnection = createEventSourceConnection(
4
+ document.location.href,
5
+ {
6
+ retryMaxAttempt: Infinity,
7
+ retryAllocatedMs: 20 * 1000,
8
+ },
9
+ )
10
+ const { status, connect, addEventCallbacks, disconnect } = eventsourceConnection
11
+ window.__server_events__ = {
12
+ addEventCallbacks,
13
+ status,
14
+ connect,
15
+ disconnect,
16
+ }
17
+ connect()
@@ -0,0 +1,48 @@
1
+ /*
2
+ * This plugin is very special because it is here
3
+ * to provide "serverEvents" used by other plugins
4
+ */
5
+
6
+ import {
7
+ parseHtmlString,
8
+ stringifyHtmlAst,
9
+ injectScriptNodeAsEarlyAsPossible,
10
+ createHtmlNode,
11
+ } from "@jsenv/ast"
12
+
13
+ const serverEventsClientFileUrl = new URL(
14
+ "./client/server_events_client.js",
15
+ import.meta.url,
16
+ ).href
17
+
18
+ export const jsenvPluginServerEventsClientInjection = () => {
19
+ return {
20
+ name: "jsenv:server_events_client_injection",
21
+ appliesDuring: "*",
22
+ transformUrlContent: {
23
+ html: (htmlUrlInfo, context) => {
24
+ const htmlAst = parseHtmlString(htmlUrlInfo.content)
25
+ const [serverEventsClientFileReference] = context.referenceUtils.inject(
26
+ {
27
+ type: "script_src",
28
+ expectedType: "js_module",
29
+ specifier: serverEventsClientFileUrl,
30
+ },
31
+ )
32
+ injectScriptNodeAsEarlyAsPossible(
33
+ htmlAst,
34
+ createHtmlNode({
35
+ "tagName": "script",
36
+ "type": "module",
37
+ "src": serverEventsClientFileReference.generatedSpecifier,
38
+ "injected-by": "jsenv:server_events",
39
+ }),
40
+ )
41
+ const htmlModified = stringifyHtmlAst(htmlAst)
42
+ return {
43
+ content: htmlModified,
44
+ }
45
+ },
46
+ },
47
+ }
48
+ }
@@ -0,0 +1,69 @@
1
+ import { createSSERoom } from "@jsenv/server"
2
+ import { createCallbackListNotifiedOnce } from "@jsenv/abort"
3
+
4
+ export const createServerEventsDispatcher = () => {
5
+ const destroyCallbackList = createCallbackListNotifiedOnce()
6
+ const rooms = []
7
+ const sseRoomLimit = 100
8
+
9
+ destroyCallbackList.add(() => {
10
+ rooms.forEach((room) => {
11
+ room.close()
12
+ })
13
+ })
14
+
15
+ return {
16
+ addRoom: (request) => {
17
+ const existingRoom = rooms.find(
18
+ (roomCandidate) =>
19
+ roomCandidate.request.ressource === request.ressource,
20
+ )
21
+ if (existingRoom) {
22
+ return existingRoom
23
+ }
24
+ const room = createSSERoom({
25
+ retryDuration: 2000,
26
+ historyLength: 100,
27
+ welcomeEventEnabled: true,
28
+ effect: () => {
29
+ rooms.push(room)
30
+ if (rooms.length >= sseRoomLimit) {
31
+ const firstRoom = rooms.shift()
32
+ firstRoom.close()
33
+ }
34
+ return () => {
35
+ // when the last client leaves the room it is closed and removed from the list
36
+ room.close()
37
+ const index = rooms.indexOf(room)
38
+ if (index > -1) {
39
+ rooms.splice(index, 1)
40
+ }
41
+ }
42
+ },
43
+ })
44
+ room.request = request
45
+ return room
46
+ },
47
+ dispatch: ({ type, data }) => {
48
+ rooms.forEach((room) =>
49
+ room.sendEventToAllClients({
50
+ type,
51
+ data: JSON.stringify(data),
52
+ }),
53
+ )
54
+ },
55
+ dispatchToRoomsMatching: ({ type, data }, predicate) => {
56
+ rooms.forEach((room) => {
57
+ if (predicate(room)) {
58
+ room.sendEventToAllClients({
59
+ type,
60
+ data: JSON.stringify(data),
61
+ })
62
+ }
63
+ })
64
+ },
65
+ destroy: () => {
66
+ destroyCallbackList.notify()
67
+ },
68
+ }
69
+ }