@jsenv/core 27.5.3 → 27.7.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.
Files changed (30) hide show
  1. package/dist/js/autoreload.js +10 -6
  2. package/dist/js/html_supervisor_installer.js +272 -163
  3. package/dist/js/html_supervisor_setup.js +10 -2
  4. package/dist/js/server_events_client.js +249 -216
  5. package/dist/js/wrapper.mjs +4233 -0
  6. package/dist/main.js +21342 -21629
  7. package/package.json +4 -4
  8. package/src/build/build.js +15 -13
  9. package/src/dev/start_dev_server.js +10 -10
  10. package/src/execute/runtimes/browsers/chromium.js +1 -1
  11. package/src/execute/runtimes/browsers/firefox.js +1 -1
  12. package/src/execute/runtimes/browsers/webkit.js +1 -1
  13. package/src/omega/kitchen.js +13 -15
  14. package/src/omega/omega_server.js +15 -0
  15. package/src/omega/server/file_service.js +11 -84
  16. package/src/omega/url_graph/url_graph_load.js +0 -2
  17. package/src/omega/url_graph/url_info_transformations.js +27 -0
  18. package/src/omega/url_graph.js +1 -0
  19. package/src/plugins/autoreload/client/autoreload.js +10 -4
  20. package/src/plugins/html_supervisor/client/error_formatter.js +187 -66
  21. package/src/plugins/html_supervisor/client/error_overlay.js +29 -31
  22. package/src/plugins/html_supervisor/client/html_supervisor_installer.js +22 -67
  23. package/src/plugins/html_supervisor/client/html_supervisor_setup.js +10 -2
  24. package/src/plugins/html_supervisor/jsenv_plugin_html_supervisor.js +96 -25
  25. package/src/plugins/server_events/client/connection_manager.js +165 -0
  26. package/src/plugins/server_events/client/event_source_connection.js +50 -256
  27. package/src/plugins/server_events/client/events_manager.js +75 -0
  28. package/src/plugins/server_events/client/server_events_client.js +12 -11
  29. package/src/plugins/server_events/client/web_socket_connection.js +81 -0
  30. package/src/plugins/server_events/server_events_dispatcher.js +70 -54
@@ -20,6 +20,7 @@ import {
20
20
  setHtmlNodeText,
21
21
  } from "@jsenv/ast"
22
22
  import { generateInlineContentUrl, stringifyUrlSite } from "@jsenv/urls"
23
+ import { getOriginalPosition } from "@jsenv/sourcemap"
23
24
 
24
25
  import { requireFromJsenv } from "@jsenv/core/src/require_from_jsenv.js"
25
26
 
@@ -46,44 +47,42 @@ export const jsenvPluginHtmlSupervisor = ({
46
47
  dev: true,
47
48
  test: true,
48
49
  },
49
- serve: (request, context) => {
50
- if (request.ressource.startsWith("/__open_in_editor__/")) {
51
- const file = request.ressource.slice("/__open_in_editor__/".length)
52
- if (!file) {
53
- return {
54
- status: 400,
55
- body: "Missing file in url",
56
- }
57
- }
58
- const launch = requireFromJsenv("launch-editor")
59
- launch(fileURLToPath(file), () => {
60
- // ignore error for now
61
- })
62
- return {
63
- status: 200,
64
- headers: {
65
- "cache-control": "no-store",
66
- },
67
- }
68
- }
50
+ serve: async (request, context) => {
69
51
  if (request.ressource.startsWith("/__get_code_frame__/")) {
70
- const url = request.ressource.slice("/__get_code_frame__/".length)
71
- const match = url.match(/:([0-9]+):([0-9]+)$/)
52
+ const { pathname, searchParams } = new URL(request.url)
53
+ const urlWithLineAndColumn = pathname.slice(
54
+ "/__get_code_frame__/".length,
55
+ )
56
+ const match = urlWithLineAndColumn.match(/:([0-9]+):([0-9]+)$/)
72
57
  if (!match) {
73
58
  return {
74
59
  status: 400,
75
60
  body: "Missing line and column in url",
76
61
  }
77
62
  }
78
- const file = url.slice(0, match.index)
79
- const line = parseInt(match[1])
80
- const column = parseInt(match[2])
63
+ const file = urlWithLineAndColumn.slice(0, match.index)
64
+ let line = parseInt(match[1])
65
+ let column = parseInt(match[2])
81
66
  const urlInfo = context.urlGraph.getUrlInfo(file)
82
67
  if (!urlInfo) {
83
68
  return {
84
69
  status: 404,
85
70
  }
86
71
  }
72
+ const remap = searchParams.has("remap")
73
+ if (remap) {
74
+ const sourcemap = urlInfo.sourcemap
75
+ if (sourcemap) {
76
+ const original = await getOriginalPosition({
77
+ sourcemap,
78
+ url: file,
79
+ line,
80
+ column,
81
+ })
82
+ line = original.line
83
+ column = original.column
84
+ }
85
+ }
87
86
  const codeFrame = stringifyUrlSite({
88
87
  url: file,
89
88
  line,
@@ -99,6 +98,78 @@ export const jsenvPluginHtmlSupervisor = ({
99
98
  body: codeFrame,
100
99
  }
101
100
  }
101
+ if (request.ressource.startsWith("/__get_error_cause__/")) {
102
+ const file = request.ressource.slice("/__get_error_cause__/".length)
103
+ if (!file) {
104
+ return {
105
+ status: 400,
106
+ body: "Missing file in url",
107
+ }
108
+ }
109
+ const getErrorCauseInfo = () => {
110
+ const urlInfo = context.urlGraph.getUrlInfo(file)
111
+ if (!urlInfo) {
112
+ return null
113
+ }
114
+ const { error } = urlInfo
115
+ if (error) {
116
+ return error
117
+ }
118
+ // search in direct dependencies (404 or 500)
119
+ const { dependencies } = urlInfo
120
+ for (const dependencyUrl of dependencies) {
121
+ const dependencyUrlInfo = context.urlGraph.getUrlInfo(dependencyUrl)
122
+ if (dependencyUrlInfo.error) {
123
+ return dependencyUrlInfo.error
124
+ }
125
+ }
126
+ return null
127
+ }
128
+ const causeInfo = getErrorCauseInfo()
129
+ const body = JSON.stringify(
130
+ causeInfo
131
+ ? {
132
+ code: causeInfo.code,
133
+ message: causeInfo.message,
134
+ reason: causeInfo.reason,
135
+ stack: errorBaseUrl
136
+ ? `stack mocked for snapshot`
137
+ : causeInfo.stack,
138
+ codeFrame: causeInfo.traceMessage,
139
+ }
140
+ : null,
141
+ null,
142
+ " ",
143
+ )
144
+ return {
145
+ status: 200,
146
+ headers: {
147
+ "cache-control": "no-cache",
148
+ "content-type": "application/json",
149
+ "content-length": Buffer.byteLength(body),
150
+ },
151
+ body,
152
+ }
153
+ }
154
+ if (request.ressource.startsWith("/__open_in_editor__/")) {
155
+ const file = request.ressource.slice("/__open_in_editor__/".length)
156
+ if (!file) {
157
+ return {
158
+ status: 400,
159
+ body: "Missing file in url",
160
+ }
161
+ }
162
+ const launch = requireFromJsenv("launch-editor")
163
+ launch(fileURLToPath(file), () => {
164
+ // ignore error for now
165
+ })
166
+ return {
167
+ status: 200,
168
+ headers: {
169
+ "cache-control": "no-store",
170
+ },
171
+ }
172
+ }
102
173
  return null
103
174
  },
104
175
  transformUrlContent: {
@@ -0,0 +1,165 @@
1
+ const READY_STATES = {
2
+ CONNECTING: "connecting",
3
+ OPEN: "open",
4
+ CLOSING: "closing",
5
+ CLOSED: "closed",
6
+ }
7
+
8
+ export const createConnectionManager = (
9
+ attemptConnection,
10
+ { retry, retryAfter, retryMaxAttempt, retryAllocatedMs },
11
+ ) => {
12
+ const readyState = {
13
+ value: READY_STATES.CLOSED,
14
+ goTo: (value) => {
15
+ if (value === readyState.value) {
16
+ return
17
+ }
18
+ readyState.value = value
19
+ readyState.onchange()
20
+ },
21
+ onchange: () => {},
22
+ }
23
+
24
+ let _disconnect = () => {}
25
+ const connect = () => {
26
+ if (
27
+ readyState.value === READY_STATES.CONNECTING ||
28
+ readyState.value === READY_STATES.OPEN
29
+ ) {
30
+ return
31
+ }
32
+
33
+ let retryCount = 0
34
+ let msSpent = 0
35
+ const attempt = () => {
36
+ readyState.goTo(READY_STATES.CONNECTING)
37
+ _disconnect = attemptConnection({
38
+ onClosed: () => {
39
+ if (!retry) {
40
+ readyState.goTo(READY_STATES.CLOSED)
41
+ console.info(`[jsenv] failed to connect to server`)
42
+ return
43
+ }
44
+ if (retryCount > retryMaxAttempt) {
45
+ readyState.goTo(READY_STATES.CLOSED)
46
+ console.info(
47
+ `[jsenv] could not connect to server after ${retryMaxAttempt} attempt`,
48
+ )
49
+ return
50
+ }
51
+ if (retryAllocatedMs && msSpent > retryAllocatedMs) {
52
+ readyState.goTo(READY_STATES.CLOSED)
53
+ console.info(
54
+ `[jsenv] could not connect to server in less than ${retryAllocatedMs}ms`,
55
+ )
56
+ return
57
+ }
58
+
59
+ // if closed while open -> connection lost
60
+ // otherwise it's the attempt to connect for the first time
61
+ // or to reconnect
62
+ if (readyState.value === READY_STATES.OPEN) {
63
+ console.info(`[jsenv] server connection lost; retrying to connect`)
64
+ }
65
+ retryCount++
66
+ setTimeout(() => {
67
+ msSpent += retryAfter
68
+ attempt()
69
+ }, retryAfter)
70
+ },
71
+ onOpen: () => {
72
+ readyState.goTo(READY_STATES.OPEN)
73
+ // console.info(`[jsenv] connected to server`)
74
+ },
75
+ })
76
+ }
77
+ attempt()
78
+ }
79
+
80
+ const disconnect = () => {
81
+ if (
82
+ readyState.value !== READY_STATES.CONNECTING &&
83
+ readyState.value !== READY_STATES.OPEN
84
+ ) {
85
+ console.warn(
86
+ `disconnect() ignored because connection is ${readyState.value}`,
87
+ )
88
+ return null
89
+ }
90
+ return _disconnect()
91
+ }
92
+
93
+ const removePageUnloadListener = listenPageUnload(() => {
94
+ if (
95
+ readyState.value === READY_STATES.CONNECTING ||
96
+ readyState.value === READY_STATES.OPEN
97
+ ) {
98
+ _disconnect()
99
+ }
100
+ })
101
+
102
+ return {
103
+ readyState,
104
+ connect,
105
+ disconnect,
106
+ destroy: () => {
107
+ removePageUnloadListener()
108
+ disconnect()
109
+ },
110
+ }
111
+ }
112
+
113
+ // const listenPageMightFreeze = (callback) => {
114
+ // const removePageHideListener = listenEvent(window, "pagehide", (pageHideEvent) => {
115
+ // if (pageHideEvent.persisted === true) {
116
+ // callback(pageHideEvent)
117
+ // }
118
+ // })
119
+ // return removePageHideListener
120
+ // }
121
+
122
+ // const listenPageFreeze = (callback) => {
123
+ // const removeFreezeListener = listenEvent(document, "freeze", (freezeEvent) => {
124
+ // callback(freezeEvent)
125
+ // })
126
+ // return removeFreezeListener
127
+ // }
128
+
129
+ // const listenPageIsRestored = (callback) => {
130
+ // const removeResumeListener = listenEvent(document, "resume", (resumeEvent) => {
131
+ // removePageshowListener()
132
+ // callback(resumeEvent)
133
+ // })
134
+ // const removePageshowListener = listenEvent(window, "pageshow", (pageshowEvent) => {
135
+ // if (pageshowEvent.persisted === true) {
136
+ // removePageshowListener()
137
+ // removeResumeListener()
138
+ // callback(pageshowEvent)
139
+ // }
140
+ // })
141
+ // return () => {
142
+ // removeResumeListener()
143
+ // removePageshowListener()
144
+ // }
145
+ // }
146
+
147
+ const listenPageUnload = (callback) => {
148
+ const removePageHideListener = listenEvent(
149
+ window,
150
+ "pagehide",
151
+ (pageHideEvent) => {
152
+ if (pageHideEvent.persisted !== true) {
153
+ callback(pageHideEvent)
154
+ }
155
+ },
156
+ )
157
+ return removePageHideListener
158
+ }
159
+
160
+ const listenEvent = (emitter, event, callback) => {
161
+ emitter.addEventListener(event, callback)
162
+ return () => {
163
+ emitter.removeEventListener(event, callback)
164
+ }
165
+ }
@@ -1,227 +1,75 @@
1
- const STATUSES = {
2
- CONNECTING: "connecting",
3
- CONNECTED: "connected",
4
- DISCONNECTED: "disconnected",
5
- }
1
+ import { createConnectionManager } from "./connection_manager.js"
2
+ import { createEventsManager } from "./events_manager.js"
6
3
 
7
4
  export const createEventSourceConnection = (
8
5
  eventSourceUrl,
9
6
  {
10
- retryMaxAttempt = Infinity,
11
- retryAllocatedMs = Infinity,
7
+ withCredentials = true,
12
8
  lastEventId,
13
9
  useEventsToManageConnection = true,
10
+ retry = false,
11
+ retryMaxAttempt = Infinity,
12
+ retryAllocatedMs = Infinity,
14
13
  } = {},
15
14
  ) => {
16
- const { EventSource } = window
17
- if (typeof EventSource !== "function") {
18
- return () => {}
19
- }
20
-
21
- let eventSource
22
- const listenersMap = new Map()
23
- const callbacksMap = new Map()
24
15
  const eventSourceOrigin = new URL(eventSourceUrl).origin
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
- })
46
- }
47
- }
48
- listenersMap.set(eventName, eventListener)
49
- if (eventSource) {
50
- eventSource.addEventListener(eventName, eventListener)
51
- }
52
- }
53
- callbacks.push(callback)
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()
16
+ const attemptConnection = ({ onOpen, onClosed }) => {
17
+ const url = lastEventId
18
+ ? addLastEventIdIntoUrlSearchParams(eventSourceUrl, lastEventId)
19
+ : eventSourceUrl
20
+ let eventSource = new EventSource(url, { withCredentials })
21
+ eventSource.onerror = () => {
22
+ eventSource.onerror = null
23
+ eventSource.onopen = null
24
+ eventSource.onmessage = null
25
+ eventSource = null
26
+ onClosed()
63
27
  }
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
- }
28
+ eventSource.onopen = () => {
29
+ eventSource.onopen = null
30
+ onOpen()
99
31
  }
100
- }
101
-
102
- const status = {
103
- value: "default",
104
- goTo: (value) => {
105
- if (value === status.value) {
106
- return
32
+ eventSource.onmessage = (messageEvent) => {
33
+ if (messageEvent.origin === eventSourceOrigin) {
34
+ if (messageEvent.lastEventId) {
35
+ lastEventId = messageEvent.lastEventId
36
+ }
37
+ const event = JSON.parse(messageEvent.data)
38
+ eventsManager.triggerCallbacks(event)
107
39
  }
108
- status.value = value
109
- status.onchange()
110
- },
111
- onchange: () => {},
112
- }
113
- let _disconnect = () => {}
114
-
115
- const attemptConnection = (url) => {
116
- if (
117
- status.value === STATUSES.CONNECTING ||
118
- status.value === STATUSES.CONNECTED
119
- ) {
120
- return
121
40
  }
122
- eventSource = new EventSource(url, {
123
- withCredentials: true,
124
- })
125
- _disconnect = () => {
126
- if (
127
- status.value !== STATUSES.CONNECTING &&
128
- status.value !== STATUSES.CONNECTED
129
- ) {
130
- console.warn(
131
- `disconnect() ignored because connection is ${status.value}`,
132
- )
133
- return
134
- }
41
+ return () => {
135
42
  if (eventSource) {
136
- eventSource.onerror = undefined
137
43
  eventSource.close()
138
- listenersMap.forEach((listener, eventName) => {
139
- eventSource.removeEventListener(eventName, listener)
140
- })
141
44
  }
142
- eventSource = null
143
- status.goTo(STATUSES.DISCONNECTED)
144
45
  }
145
- let retryCount = 0
146
- let firstRetryMs = Date.now()
147
- eventSource.onerror = (errorEvent) => {
148
- if (errorEvent.target.readyState === EventSource.CONNECTING) {
149
- if (retryCount > retryMaxAttempt) {
150
- console.info(`could not connect after ${retryMaxAttempt} attempt`)
151
- _disconnect()
152
- return
153
- }
154
-
155
- if (retryCount === 0) {
156
- firstRetryMs = Date.now()
157
- } else {
158
- const allRetryDuration = Date.now() - firstRetryMs
159
- if (retryAllocatedMs && allRetryDuration > retryAllocatedMs) {
160
- console.info(
161
- `could not connect in less than ${retryAllocatedMs} ms`,
162
- )
163
- _disconnect()
164
- return
165
- }
46
+ }
47
+ const connectionManager = createConnectionManager(attemptConnection, {
48
+ retry,
49
+ retryMaxAttempt,
50
+ retryAllocatedMs,
51
+ })
52
+ const eventsManager = createEventsManager({
53
+ effect: () => {
54
+ if (useEventsToManageConnection) {
55
+ connectionManager.connect()
56
+ return () => {
57
+ connectionManager.disconnect()
166
58
  }
167
-
168
- retryCount++
169
- status.goTo(STATUSES.CONNECTING)
170
- return
171
- }
172
-
173
- if (errorEvent.target.readyState === EventSource.CLOSED) {
174
- _disconnect()
175
- return
176
59
  }
177
- }
178
- eventSource.onopen = () => {
179
- status.goTo(STATUSES.CONNECTED)
180
- }
181
- listenersMap.forEach((listener, eventName) => {
182
- eventSource.addEventListener(eventName, listener)
183
- })
184
- if (!listenersMap.has("welcome")) {
185
- addEventCallbacks({
186
- welcome: () => {}, // to update lastEventId
187
- })
188
- }
189
- status.goTo(STATUSES.CONNECTING)
190
- }
191
-
192
- let _connect = () => {
193
- attemptConnection(eventSourceUrl)
194
- _connect = () => {
195
- attemptConnection(
196
- lastEventId
197
- ? addLastEventIdIntoUrlSearchParams(eventSourceUrl, lastEventId)
198
- : eventSourceUrl,
199
- )
200
- }
201
- }
202
-
203
- const removePageUnloadListener = listenPageUnload(() => {
204
- if (
205
- status.value === STATUSES.CONNECTING ||
206
- status.value === STATUSES.CONNECTED
207
- ) {
208
- _disconnect()
209
- }
60
+ return null
61
+ },
210
62
  })
211
63
 
212
- const destroy = () => {
213
- removePageUnloadListener()
214
- _disconnect()
215
- listenersMap.clear()
216
- callbacksMap.clear()
217
- }
218
-
219
64
  return {
220
- status,
221
- connect: () => _connect(),
222
- addEventCallbacks,
223
- disconnect: () => _disconnect(),
224
- destroy,
65
+ readyState: connectionManager.readyState,
66
+ listenEvents: (namedCallbacks) => {
67
+ return eventsManager.addCallbacks(namedCallbacks)
68
+ },
69
+ destroy: () => {
70
+ connectionManager.destroy()
71
+ eventsManager.destroy()
72
+ },
225
73
  }
226
74
  }
227
75
 
@@ -233,57 +81,3 @@ const addLastEventIdIntoUrlSearchParams = (url, lastEventId) => {
233
81
  }
234
82
  return `${url}last-event-id=${encodeURIComponent(lastEventId)}`
235
83
  }
236
-
237
- // const listenPageMightFreeze = (callback) => {
238
- // const removePageHideListener = listenEvent(window, "pagehide", (pageHideEvent) => {
239
- // if (pageHideEvent.persisted === true) {
240
- // callback(pageHideEvent)
241
- // }
242
- // })
243
- // return removePageHideListener
244
- // }
245
-
246
- // const listenPageFreeze = (callback) => {
247
- // const removeFreezeListener = listenEvent(document, "freeze", (freezeEvent) => {
248
- // callback(freezeEvent)
249
- // })
250
- // return removeFreezeListener
251
- // }
252
-
253
- // const listenPageIsRestored = (callback) => {
254
- // const removeResumeListener = listenEvent(document, "resume", (resumeEvent) => {
255
- // removePageshowListener()
256
- // callback(resumeEvent)
257
- // })
258
- // const removePageshowListener = listenEvent(window, "pageshow", (pageshowEvent) => {
259
- // if (pageshowEvent.persisted === true) {
260
- // removePageshowListener()
261
- // removeResumeListener()
262
- // callback(pageshowEvent)
263
- // }
264
- // })
265
- // return () => {
266
- // removeResumeListener()
267
- // removePageshowListener()
268
- // }
269
- // }
270
-
271
- const listenPageUnload = (callback) => {
272
- const removePageHideListener = listenEvent(
273
- window,
274
- "pagehide",
275
- (pageHideEvent) => {
276
- if (pageHideEvent.persisted !== true) {
277
- callback(pageHideEvent)
278
- }
279
- },
280
- )
281
- return removePageHideListener
282
- }
283
-
284
- const listenEvent = (emitter, event, callback) => {
285
- emitter.addEventListener(event, callback)
286
- return () => {
287
- emitter.removeEventListener(event, callback)
288
- }
289
- }