@jsenv/core 41.5.21 → 41.5.23
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/dist/build/build.js +9 -19
- package/dist/client/server_events/server_events_client.js +57 -41
- package/dist/start_dev_server/start_dev_server.js +183 -54
- package/package.json +2 -2
- package/src/dev/start_dev_server.js +22 -2
- package/src/helpers/watch_dependencies.js +156 -34
- package/src/plugins/package_side_effects/jsenv_plugin_package_side_effects.js +9 -19
- package/src/plugins/server_events/client/server_events_client.js +57 -41
package/dist/build/build.js
CHANGED
|
@@ -10128,17 +10128,16 @@ const jsenvPluginAutoreloadOnServerRestart = () => {
|
|
|
10128
10128
|
};
|
|
10129
10129
|
};
|
|
10130
10130
|
|
|
10131
|
-
|
|
10132
|
-
*
|
|
10133
|
-
*
|
|
10134
|
-
*
|
|
10135
|
-
*
|
|
10136
|
-
*
|
|
10137
|
-
* qui ne déclare pas le field side effect afin
|
|
10138
|
-
* d'override le package.json du project qui lui dit qu'il ny en a pas
|
|
10139
|
-
*
|
|
10140
|
-
* On part du principe pour le moment que c'est la respo du package racine de déclarer cela
|
|
10131
|
+
/*
|
|
10132
|
+
* Tells the bundler which modules may be dropped when nothing imports a value
|
|
10133
|
+
* from them, from the "sideEffects" field of the package.json closest to each
|
|
10134
|
+
* file — a dependency's own declaration for its files, the project's for its
|
|
10135
|
+
* own. A package that declares nothing keeps every module (the bundler's own
|
|
10136
|
+
* default), so an undeclared root only loses precision, never code.
|
|
10141
10137
|
*
|
|
10138
|
+
* Whether the project's package.json is written back with the side-effect
|
|
10139
|
+
* files this build produced is decided in build.js, and only for a project that
|
|
10140
|
+
* already declares the field; it is not this plugin's concern.
|
|
10142
10141
|
*/
|
|
10143
10142
|
|
|
10144
10143
|
|
|
@@ -10146,15 +10145,6 @@ const jsenvPluginPackageSideEffects = ({ packageDirectory }) => {
|
|
|
10146
10145
|
if (!packageDirectory.url) {
|
|
10147
10146
|
return [];
|
|
10148
10147
|
}
|
|
10149
|
-
const packageJson = packageDirectory.read(packageDirectory.url);
|
|
10150
|
-
if (!packageJson) {
|
|
10151
|
-
return [];
|
|
10152
|
-
}
|
|
10153
|
-
const { sideEffects } = packageJson;
|
|
10154
|
-
if (sideEffects !== false && !Array.isArray(sideEffects)) {
|
|
10155
|
-
return [];
|
|
10156
|
-
}
|
|
10157
|
-
|
|
10158
10148
|
const packageSideEffectsCacheMap = new Map();
|
|
10159
10149
|
const readSideEffectInfoFromClosestPackage = (urlInfo) => {
|
|
10160
10150
|
const closestPackageDirectoryUrl = urlInfo.packageDirectoryUrl;
|
|
@@ -4,7 +4,7 @@ let createEventsManager;
|
|
|
4
4
|
const callbacksMap = new Map();
|
|
5
5
|
let cleanup;
|
|
6
6
|
const addCallbacks = (namedCallbacks) => {
|
|
7
|
-
|
|
7
|
+
const sizeBeforeAdd = callbacksMap.size;
|
|
8
8
|
Object.keys(namedCallbacks).forEach((eventName) => {
|
|
9
9
|
const callback = namedCallbacks[eventName];
|
|
10
10
|
const existingCallbacks = callbacksMap.get(eventName);
|
|
@@ -17,7 +17,10 @@ let createEventsManager;
|
|
|
17
17
|
}
|
|
18
18
|
callbacks.push(callback);
|
|
19
19
|
});
|
|
20
|
-
|
|
20
|
+
// the effect owns whatever the callbacks need to exist (the connection),
|
|
21
|
+
// so it runs on the edge into "someone is listening" and its cleanup runs
|
|
22
|
+
// on the edge back out of it
|
|
23
|
+
if (sizeBeforeAdd === 0 && callbacksMap.size > 0) {
|
|
21
24
|
cleanup = effect();
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -25,7 +28,7 @@ let createEventsManager;
|
|
|
25
28
|
return () => {
|
|
26
29
|
if (removed) return;
|
|
27
30
|
removed = true;
|
|
28
|
-
|
|
31
|
+
const sizeBeforeRemove = callbacksMap.size;
|
|
29
32
|
Object.keys(namedCallbacks).forEach((eventName) => {
|
|
30
33
|
const callback = namedCallbacks[eventName];
|
|
31
34
|
const callbacks = callbacksMap.get(eventName);
|
|
@@ -40,12 +43,7 @@ let createEventsManager;
|
|
|
40
43
|
}
|
|
41
44
|
});
|
|
42
45
|
namedCallbacks = null; // allow garbage collect
|
|
43
|
-
if (
|
|
44
|
-
cleanup &&
|
|
45
|
-
typeof cleanup === "function" &&
|
|
46
|
-
callbacksMapSize > 0 &&
|
|
47
|
-
callbacksMapSize.size === 0
|
|
48
|
-
) {
|
|
46
|
+
if (cleanup && sizeBeforeRemove > 0 && callbacksMap.size === 0) {
|
|
49
47
|
cleanup();
|
|
50
48
|
cleanup = null;
|
|
51
49
|
}
|
|
@@ -85,9 +83,14 @@ let createConnectionManager;
|
|
|
85
83
|
CLOSED: "closed",
|
|
86
84
|
};
|
|
87
85
|
|
|
86
|
+
// The peer is a dev server being restarted, not a flaky network: there is no
|
|
87
|
+
// attempt count or time budget past which giving up would be correct. Attempts
|
|
88
|
+
// space out to retryAfterMax and never stop, and the moments where the server
|
|
89
|
+
// is most likely to be back already (tab visible, window focused, network
|
|
90
|
+
// back) skip the wait instead of leaving a detached page on screen.
|
|
88
91
|
createConnectionManager = (
|
|
89
92
|
attemptConnection,
|
|
90
|
-
{ logs, retry, retryAfter,
|
|
93
|
+
{ logs, retry, retryAfter, retryAfterMax },
|
|
91
94
|
) => {
|
|
92
95
|
const readyState = {
|
|
93
96
|
value: READY_STATES.CLOSED,
|
|
@@ -102,6 +105,7 @@ let createConnectionManager;
|
|
|
102
105
|
};
|
|
103
106
|
|
|
104
107
|
let _disconnect = () => {};
|
|
108
|
+
let _retryNow = () => {};
|
|
105
109
|
const connect = () => {
|
|
106
110
|
if (
|
|
107
111
|
readyState.value === READY_STATES.CONNECTING ||
|
|
@@ -110,11 +114,11 @@ let createConnectionManager;
|
|
|
110
114
|
return;
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
let
|
|
114
|
-
let
|
|
117
|
+
let retryDelay = retryAfter;
|
|
118
|
+
let retryTimeout = null;
|
|
115
119
|
const attempt = () => {
|
|
120
|
+
retryTimeout = null;
|
|
116
121
|
readyState.goTo(READY_STATES.CONNECTING);
|
|
117
|
-
let timeout;
|
|
118
122
|
const cancelAttempt = attemptConnection({
|
|
119
123
|
onClosed: () => {
|
|
120
124
|
if (!retry) {
|
|
@@ -124,24 +128,6 @@ let createConnectionManager;
|
|
|
124
128
|
}
|
|
125
129
|
return;
|
|
126
130
|
}
|
|
127
|
-
if (retryCount > retryMaxAttempt) {
|
|
128
|
-
readyState.goTo(READY_STATES.CLOSED);
|
|
129
|
-
if (logs) {
|
|
130
|
-
console.info(
|
|
131
|
-
`[jsenv] could not connect to server after ${retryMaxAttempt} attempt`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (retryAllocatedMs && msSpent > retryAllocatedMs) {
|
|
137
|
-
readyState.goTo(READY_STATES.CLOSED);
|
|
138
|
-
if (logs) {
|
|
139
|
-
console.info(
|
|
140
|
-
`[jsenv] could not connect to server in less than ${retryAllocatedMs}ms`,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
131
|
// if closed while open -> connection lost
|
|
146
132
|
// otherwise it's the attempt to connect for the first time
|
|
147
133
|
// or to reconnect
|
|
@@ -152,22 +138,31 @@ let createConnectionManager;
|
|
|
152
138
|
);
|
|
153
139
|
}
|
|
154
140
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}, retryAfter);
|
|
141
|
+
retryTimeout = setTimeout(attempt, retryDelay);
|
|
142
|
+
const nextRetryDelay = retryDelay * 2;
|
|
143
|
+
retryDelay =
|
|
144
|
+
nextRetryDelay > retryAfterMax ? retryAfterMax : nextRetryDelay;
|
|
160
145
|
},
|
|
161
146
|
onOpen: () => {
|
|
147
|
+
retryDelay = retryAfter;
|
|
162
148
|
readyState.goTo(READY_STATES.OPEN);
|
|
163
149
|
},
|
|
164
150
|
});
|
|
165
151
|
_disconnect = () => {
|
|
166
152
|
cancelAttempt();
|
|
167
|
-
clearTimeout(
|
|
153
|
+
clearTimeout(retryTimeout);
|
|
154
|
+
retryTimeout = null;
|
|
168
155
|
readyState.goTo(READY_STATES.CLOSED);
|
|
169
156
|
};
|
|
170
157
|
};
|
|
158
|
+
_retryNow = () => {
|
|
159
|
+
if (retryTimeout === null) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
clearTimeout(retryTimeout);
|
|
163
|
+
retryDelay = retryAfter;
|
|
164
|
+
attempt();
|
|
165
|
+
};
|
|
171
166
|
attempt();
|
|
172
167
|
};
|
|
173
168
|
|
|
@@ -194,6 +189,9 @@ let createConnectionManager;
|
|
|
194
189
|
_disconnect();
|
|
195
190
|
}
|
|
196
191
|
});
|
|
192
|
+
const removeServerMightBeBackListeners = listenServerMightBeBack(() => {
|
|
193
|
+
_retryNow();
|
|
194
|
+
});
|
|
197
195
|
|
|
198
196
|
return {
|
|
199
197
|
readyState,
|
|
@@ -201,11 +199,31 @@ let createConnectionManager;
|
|
|
201
199
|
disconnect,
|
|
202
200
|
destroy: () => {
|
|
203
201
|
removePageUnloadListener();
|
|
202
|
+
removeServerMightBeBackListeners();
|
|
204
203
|
disconnect();
|
|
205
204
|
},
|
|
206
205
|
};
|
|
207
206
|
};
|
|
208
207
|
|
|
208
|
+
const listenServerMightBeBack = (callback) => {
|
|
209
|
+
const removeOnlineListener = listenEvent(window, "online", callback);
|
|
210
|
+
const removeFocusListener = listenEvent(window, "focus", callback);
|
|
211
|
+
const removeVisibilityChangeListener = listenEvent(
|
|
212
|
+
document,
|
|
213
|
+
"visibilitychange",
|
|
214
|
+
() => {
|
|
215
|
+
if (document.visibilityState === "visible") {
|
|
216
|
+
callback();
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
return () => {
|
|
221
|
+
removeOnlineListener();
|
|
222
|
+
removeFocusListener();
|
|
223
|
+
removeVisibilityChangeListener();
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
209
227
|
// const listenPageMightFreeze = (callback) => {
|
|
210
228
|
// const removePageHideListener = listenEvent(window, "pagehide", (pageHideEvent) => {
|
|
211
229
|
// if (pageHideEvent.persisted === true) {
|
|
@@ -271,8 +289,7 @@ let createWebSocketConnection;
|
|
|
271
289
|
useEventsToManageConnection = true,
|
|
272
290
|
retry = false,
|
|
273
291
|
retryAfter = 1000,
|
|
274
|
-
|
|
275
|
-
retryAllocatedMs = Infinity,
|
|
292
|
+
retryAfterMax = 5000,
|
|
276
293
|
} = {},
|
|
277
294
|
) => {
|
|
278
295
|
const connectionManager = createConnectionManager(
|
|
@@ -315,7 +332,7 @@ let createWebSocketConnection;
|
|
|
315
332
|
}
|
|
316
333
|
};
|
|
317
334
|
},
|
|
318
|
-
{ logs, retry, retryAfter,
|
|
335
|
+
{ logs, retry, retryAfter, retryAfterMax },
|
|
319
336
|
);
|
|
320
337
|
const eventsManager = createEventsManager({
|
|
321
338
|
effect: () => {
|
|
@@ -439,7 +456,6 @@ const serverEventsInterface = {
|
|
|
439
456
|
const websocketConnection = createWebSocketConnection(websocketUrl, {
|
|
440
457
|
logs,
|
|
441
458
|
retry: true,
|
|
442
|
-
retryAllocatedMs: 10_000,
|
|
443
459
|
});
|
|
444
460
|
|
|
445
461
|
const { readyState, connect, disconnect, listenEvents } =
|
|
@@ -236,17 +236,29 @@ const isExactVersion = (declaredVersion) => {
|
|
|
236
236
|
};
|
|
237
237
|
|
|
238
238
|
/*
|
|
239
|
-
* Detects
|
|
240
|
-
*
|
|
241
|
-
*
|
|
239
|
+
* Detects the moment "npm install" changes what the running pages get from
|
|
240
|
+
* node_modules, so the browser can be reloaded at that moment:
|
|
241
|
+
*
|
|
242
|
+
* - a declared dependency stops being missing or outdated: what package.json
|
|
243
|
+
* asks for is there;
|
|
244
|
+
* - a package the pages were served changes version: the "?v=" baked into the
|
|
245
|
+
* urls they evaluated names a version node_modules no longer holds. Left
|
|
246
|
+
* alone, the next hot reload of a file importing that package resolves it to
|
|
247
|
+
* the new version and the page evaluates a second copy of the package next
|
|
248
|
+
* to the first, two module scopes for something meant to exist once.
|
|
242
249
|
*
|
|
243
250
|
* node_modules is deliberately not watched: it is far too big, and an install
|
|
244
251
|
* rewrites, dedupes and moves package directories around, so a watcher placed
|
|
245
|
-
* on one of them is unreliable. Instead
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
252
|
+
* on one of them is unreliable. Instead a few package.json files are polled,
|
|
253
|
+
* at the cost of a stat each: the packages known to be missing or outdated,
|
|
254
|
+
* and the packages the pages were served, whose package.json the url graph
|
|
255
|
+
* keeps (that is where "?v=" comes from). The project package.json is watched
|
|
256
|
+
* though: it is a single file, and editing it is what puts a dependency out of
|
|
257
|
+
* date in the first place.
|
|
258
|
+
*
|
|
259
|
+
* Both detections run in the same tick so that an install fixing an outdated
|
|
260
|
+
* package the page runs is reported once: two reports would be two reloads,
|
|
261
|
+
* the second one landing on the page that just came back.
|
|
250
262
|
*/
|
|
251
263
|
|
|
252
264
|
|
|
@@ -254,7 +266,13 @@ const POLL_INTERVAL = 500;
|
|
|
254
266
|
|
|
255
267
|
const watchDependencies = (
|
|
256
268
|
packageDirectory,
|
|
257
|
-
{
|
|
269
|
+
{
|
|
270
|
+
getKitchens = () => [],
|
|
271
|
+
onProblem,
|
|
272
|
+
onInstalled,
|
|
273
|
+
onChange,
|
|
274
|
+
pollInterval = POLL_INTERVAL,
|
|
275
|
+
},
|
|
258
276
|
) => {
|
|
259
277
|
let problemMap = new Map();
|
|
260
278
|
// every path given to the browser is relative to the package directory, the
|
|
@@ -269,11 +287,49 @@ const watchDependencies = (
|
|
|
269
287
|
if (!packageDirectory.url) {
|
|
270
288
|
return watcher;
|
|
271
289
|
}
|
|
272
|
-
let timer = null;
|
|
273
290
|
|
|
274
|
-
const
|
|
291
|
+
const checkServedVersions = (installMap) => {
|
|
292
|
+
for (const kitchen of getKitchens()) {
|
|
293
|
+
for (const urlInfo of kitchen.graph.urlInfoMap.values()) {
|
|
294
|
+
const served = readServedVersion(urlInfo);
|
|
295
|
+
if (!served) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const installedPackageJson = readInstalledPackageJson(
|
|
299
|
+
packageDirectory,
|
|
300
|
+
served.directoryUrl,
|
|
301
|
+
);
|
|
302
|
+
if (!installedPackageJson) {
|
|
303
|
+
// the package is being rewritten, what it becomes is known once it is back
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const installedVersion = installedPackageJson.version;
|
|
307
|
+
if (installedVersion === served.version) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
// the graph learns the file moved, as it would from a watcher: the
|
|
311
|
+
// files importing this package are cooked again with the new "?v="
|
|
312
|
+
// when the page comes back
|
|
313
|
+
urlInfo.onModified();
|
|
314
|
+
if (installMap.has(served.packageName)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
installMap.set(served.packageName, {
|
|
318
|
+
packageName: served.packageName,
|
|
319
|
+
installedVersion,
|
|
320
|
+
servedVersion: served.version,
|
|
321
|
+
declaredVersion: null,
|
|
322
|
+
severity: "warning",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const checkDeclared = (installMap) => {
|
|
329
|
+
const statusMap = new Map();
|
|
275
330
|
const nextProblemMap = new Map();
|
|
276
331
|
for (const status of readDependencyStatuses(packageDirectory)) {
|
|
332
|
+
statusMap.set(status.packageName, status);
|
|
277
333
|
if (status.state === "missing" || status.state === "outdated") {
|
|
278
334
|
nextProblemMap.set(status.packageName, status);
|
|
279
335
|
}
|
|
@@ -289,9 +345,21 @@ const watchDependencies = (
|
|
|
289
345
|
}
|
|
290
346
|
}
|
|
291
347
|
for (const [packageName, previousStatus] of problemMap) {
|
|
292
|
-
if (
|
|
293
|
-
|
|
348
|
+
if (nextProblemMap.has(packageName)) {
|
|
349
|
+
continue;
|
|
294
350
|
}
|
|
351
|
+
const install = installMap.get(packageName);
|
|
352
|
+
if (install) {
|
|
353
|
+
install.declaredVersion = previousStatus.declaredVersion;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
installMap.set(packageName, {
|
|
357
|
+
packageName,
|
|
358
|
+
installedVersion: statusMap.get(packageName).installedVersion,
|
|
359
|
+
servedVersion: null,
|
|
360
|
+
declaredVersion: previousStatus.declaredVersion,
|
|
361
|
+
severity: previousStatus.severity,
|
|
362
|
+
});
|
|
295
363
|
}
|
|
296
364
|
const changed =
|
|
297
365
|
nextProblemMap.size !== problemMap.size ||
|
|
@@ -309,45 +377,96 @@ const watchDependencies = (
|
|
|
309
377
|
if (changed) {
|
|
310
378
|
onChange(watcher.getProblems());
|
|
311
379
|
}
|
|
312
|
-
if (problemMap.size === 0) {
|
|
313
|
-
stopPolling();
|
|
314
|
-
} else {
|
|
315
|
-
startPolling();
|
|
316
|
-
}
|
|
317
380
|
};
|
|
318
381
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
382
|
+
// The declared dependencies are compared with node_modules only when
|
|
383
|
+
// something can have changed for them: the project package.json was edited,
|
|
384
|
+
// or a problem is known and an install may be fixing it. Comparing them at
|
|
385
|
+
// every tick would catch installs halfway and report a package as missing
|
|
386
|
+
// while it is being rewritten.
|
|
387
|
+
const check = ({ declared }) => {
|
|
388
|
+
const installMap = new Map();
|
|
389
|
+
checkServedVersions(installMap);
|
|
390
|
+
if (declared || problemMap.size > 0) {
|
|
391
|
+
checkDeclared(installMap);
|
|
322
392
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
};
|
|
326
|
-
const stopPolling = () => {
|
|
327
|
-
if (!timer) {
|
|
328
|
-
return;
|
|
393
|
+
for (const install of installMap.values()) {
|
|
394
|
+
onInstalled(install);
|
|
329
395
|
}
|
|
330
|
-
clearInterval(timer);
|
|
331
|
-
timer = null;
|
|
332
396
|
};
|
|
333
397
|
|
|
334
398
|
const unwatchPackageJson = registerFileLifecycle(
|
|
335
399
|
new URL("package.json", packageDirectory.url),
|
|
336
400
|
{
|
|
337
|
-
added: check,
|
|
338
|
-
updated: check,
|
|
401
|
+
added: () => check({ declared: true }),
|
|
402
|
+
updated: () => check({ declared: true }),
|
|
339
403
|
keepProcessAlive: false,
|
|
340
404
|
},
|
|
341
405
|
);
|
|
342
|
-
check();
|
|
406
|
+
check({ declared: true });
|
|
407
|
+
const timer = setInterval(() => {
|
|
408
|
+
check({ declared: false });
|
|
409
|
+
}, pollInterval);
|
|
410
|
+
timer.unref();
|
|
343
411
|
|
|
344
412
|
watcher.stop = () => {
|
|
345
|
-
|
|
413
|
+
clearInterval(timer);
|
|
346
414
|
unwatchPackageJson();
|
|
347
415
|
};
|
|
348
416
|
return watcher;
|
|
349
417
|
};
|
|
350
418
|
|
|
419
|
+
// The url graph keeps the content of every package.json it resolved a bare
|
|
420
|
+
// specifier against, and that content holds the version the page received.
|
|
421
|
+
// It is parsed once per content: the same string is seen at every tick.
|
|
422
|
+
const servedVersionCache = new WeakMap();
|
|
423
|
+
const readServedVersion = (urlInfo) => {
|
|
424
|
+
const { url, content } = urlInfo;
|
|
425
|
+
if (content === undefined) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
if (!url.startsWith("file:") || !url.endsWith("/package.json")) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const nodeModulesIndex = url.lastIndexOf("/node_modules/");
|
|
432
|
+
if (nodeModulesIndex === -1) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
// dereferenced url infos stay in the graph; nothing imports this package anymore
|
|
436
|
+
if (urlInfo.referenceFromOthersSet.size === 0) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
const fromCache = servedVersionCache.get(urlInfo);
|
|
440
|
+
if (fromCache && fromCache.content === content) {
|
|
441
|
+
return fromCache;
|
|
442
|
+
}
|
|
443
|
+
let packageJson;
|
|
444
|
+
try {
|
|
445
|
+
packageJson = JSON.parse(content);
|
|
446
|
+
} catch {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const served = {
|
|
450
|
+
content,
|
|
451
|
+
directoryUrl: new URL("./", url).href,
|
|
452
|
+
packageName: packageNameFromSpecifier(
|
|
453
|
+
url.slice(nodeModulesIndex + "/node_modules/".length),
|
|
454
|
+
),
|
|
455
|
+
version: packageJson.version,
|
|
456
|
+
};
|
|
457
|
+
servedVersionCache.set(urlInfo, served);
|
|
458
|
+
return served;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// an install in progress can be caught halfway, with a package.json not written yet
|
|
462
|
+
const readInstalledPackageJson = (packageDirectory, directoryUrl) => {
|
|
463
|
+
try {
|
|
464
|
+
return packageDirectory.read(directoryUrl);
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
351
470
|
const jsenvCoreDirectoryUrl = new URL("../", import.meta.url);
|
|
352
471
|
|
|
353
472
|
const createPackageDirectory = ({
|
|
@@ -7909,17 +8028,16 @@ const jsenvPluginAutoreloadOnServerRestart = () => {
|
|
|
7909
8028
|
};
|
|
7910
8029
|
};
|
|
7911
8030
|
|
|
7912
|
-
|
|
7913
|
-
*
|
|
7914
|
-
*
|
|
7915
|
-
*
|
|
7916
|
-
*
|
|
7917
|
-
*
|
|
7918
|
-
* qui ne déclare pas le field side effect afin
|
|
7919
|
-
* d'override le package.json du project qui lui dit qu'il ny en a pas
|
|
7920
|
-
*
|
|
7921
|
-
* On part du principe pour le moment que c'est la respo du package racine de déclarer cela
|
|
8031
|
+
/*
|
|
8032
|
+
* Tells the bundler which modules may be dropped when nothing imports a value
|
|
8033
|
+
* from them, from the "sideEffects" field of the package.json closest to each
|
|
8034
|
+
* file — a dependency's own declaration for its files, the project's for its
|
|
8035
|
+
* own. A package that declares nothing keeps every module (the bundler's own
|
|
8036
|
+
* default), so an undeclared root only loses precision, never code.
|
|
7922
8037
|
*
|
|
8038
|
+
* Whether the project's package.json is written back with the side-effect
|
|
8039
|
+
* files this build produced is decided in build.js, and only for a project that
|
|
8040
|
+
* already declares the field; it is not this plugin's concern.
|
|
7923
8041
|
*/
|
|
7924
8042
|
|
|
7925
8043
|
|
|
@@ -7927,15 +8045,6 @@ const jsenvPluginPackageSideEffects = ({ packageDirectory }) => {
|
|
|
7927
8045
|
if (!packageDirectory.url) {
|
|
7928
8046
|
return [];
|
|
7929
8047
|
}
|
|
7930
|
-
const packageJson = packageDirectory.read(packageDirectory.url);
|
|
7931
|
-
if (!packageJson) {
|
|
7932
|
-
return [];
|
|
7933
|
-
}
|
|
7934
|
-
const { sideEffects } = packageJson;
|
|
7935
|
-
if (sideEffects !== false && !Array.isArray(sideEffects)) {
|
|
7936
|
-
return [];
|
|
7937
|
-
}
|
|
7938
|
-
|
|
7939
8048
|
const packageSideEffectsCacheMap = new Map();
|
|
7940
8049
|
const readSideEffectInfoFromClosestPackage = (urlInfo) => {
|
|
7941
8050
|
const closestPackageDirectoryUrl = urlInfo.packageDirectoryUrl;
|
|
@@ -12584,6 +12693,7 @@ const startDevServer = async ({
|
|
|
12584
12693
|
};
|
|
12585
12694
|
const dependencyProblemEventEmitter = createEventEmitter();
|
|
12586
12695
|
const dependencyWatcher = watchDependencies(packageDirectory, {
|
|
12696
|
+
getKitchens: () => kitchenCache.values(),
|
|
12587
12697
|
onChange: (problems) => {
|
|
12588
12698
|
dependencyProblemEventEmitter.emit(problems);
|
|
12589
12699
|
},
|
|
@@ -12605,11 +12715,30 @@ const startDevServer = async ({
|
|
|
12605
12715
|
logger.info(message);
|
|
12606
12716
|
}
|
|
12607
12717
|
},
|
|
12608
|
-
onInstalled: ({
|
|
12609
|
-
|
|
12718
|
+
onInstalled: ({
|
|
12719
|
+
packageName,
|
|
12720
|
+
installedVersion,
|
|
12721
|
+
servedVersion,
|
|
12722
|
+
declaredVersion,
|
|
12723
|
+
severity,
|
|
12724
|
+
}) => {
|
|
12725
|
+
if (servedVersion) {
|
|
12726
|
+
logger.info(
|
|
12727
|
+
`"${packageName}@${installedVersion}" is now installed, the page runs ${servedVersion}`,
|
|
12728
|
+
);
|
|
12729
|
+
} else {
|
|
12730
|
+
logger.info(`"${packageName}@${declaredVersion}" is now installed`);
|
|
12731
|
+
}
|
|
12610
12732
|
if (severity !== "warning") {
|
|
12611
12733
|
return;
|
|
12612
12734
|
}
|
|
12735
|
+
if (servedVersion) {
|
|
12736
|
+
reloadRequestEventEmitter.emit({
|
|
12737
|
+
cause: `${packageName} ${servedVersion} -> ${installedVersion} installed`,
|
|
12738
|
+
reason: `a dependency the page runs changed version in node_modules`,
|
|
12739
|
+
});
|
|
12740
|
+
return;
|
|
12741
|
+
}
|
|
12613
12742
|
reloadRequestEventEmitter.emit({
|
|
12614
12743
|
cause: `${packageName}@${declaredVersion} installed`,
|
|
12615
12744
|
reason: `a dependency became available in node_modules`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "41.5.
|
|
3
|
+
"version": "41.5.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tool to develop, test and build js projects",
|
|
6
6
|
"repository": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"@jsenv/plugin-minification": "1.7.22",
|
|
80
80
|
"@jsenv/plugin-supervisor": "1.8.24",
|
|
81
81
|
"@jsenv/plugin-transpilation": "1.7.9",
|
|
82
|
-
"@jsenv/server": "17.6.
|
|
82
|
+
"@jsenv/server": "17.6.5",
|
|
83
83
|
"@jsenv/sourcemap": "1.4.9",
|
|
84
84
|
"react-table": "7.8.0"
|
|
85
85
|
},
|
|
@@ -229,6 +229,7 @@ export const startDevServer = async ({
|
|
|
229
229
|
};
|
|
230
230
|
const dependencyProblemEventEmitter = createEventEmitter();
|
|
231
231
|
const dependencyWatcher = watchDependencies(packageDirectory, {
|
|
232
|
+
getKitchens: () => kitchenCache.values(),
|
|
232
233
|
onChange: (problems) => {
|
|
233
234
|
dependencyProblemEventEmitter.emit(problems);
|
|
234
235
|
},
|
|
@@ -250,11 +251,30 @@ export const startDevServer = async ({
|
|
|
250
251
|
logger.info(message);
|
|
251
252
|
}
|
|
252
253
|
},
|
|
253
|
-
onInstalled: ({
|
|
254
|
-
|
|
254
|
+
onInstalled: ({
|
|
255
|
+
packageName,
|
|
256
|
+
installedVersion,
|
|
257
|
+
servedVersion,
|
|
258
|
+
declaredVersion,
|
|
259
|
+
severity,
|
|
260
|
+
}) => {
|
|
261
|
+
if (servedVersion) {
|
|
262
|
+
logger.info(
|
|
263
|
+
`"${packageName}@${installedVersion}" is now installed, the page runs ${servedVersion}`,
|
|
264
|
+
);
|
|
265
|
+
} else {
|
|
266
|
+
logger.info(`"${packageName}@${declaredVersion}" is now installed`);
|
|
267
|
+
}
|
|
255
268
|
if (severity !== "warning") {
|
|
256
269
|
return;
|
|
257
270
|
}
|
|
271
|
+
if (servedVersion) {
|
|
272
|
+
reloadRequestEventEmitter.emit({
|
|
273
|
+
cause: `${packageName} ${servedVersion} -> ${installedVersion} installed`,
|
|
274
|
+
reason: `a dependency the page runs changed version in node_modules`,
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
258
278
|
reloadRequestEventEmitter.emit({
|
|
259
279
|
cause: `${packageName}@${declaredVersion} installed`,
|
|
260
280
|
reason: `a dependency became available in node_modules`,
|
|
@@ -1,26 +1,47 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Detects
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Detects the moment "npm install" changes what the running pages get from
|
|
3
|
+
* node_modules, so the browser can be reloaded at that moment:
|
|
4
|
+
*
|
|
5
|
+
* - a declared dependency stops being missing or outdated: what package.json
|
|
6
|
+
* asks for is there;
|
|
7
|
+
* - a package the pages were served changes version: the "?v=" baked into the
|
|
8
|
+
* urls they evaluated names a version node_modules no longer holds. Left
|
|
9
|
+
* alone, the next hot reload of a file importing that package resolves it to
|
|
10
|
+
* the new version and the page evaluates a second copy of the package next
|
|
11
|
+
* to the first, two module scopes for something meant to exist once.
|
|
5
12
|
*
|
|
6
13
|
* node_modules is deliberately not watched: it is far too big, and an install
|
|
7
14
|
* rewrites, dedupes and moves package directories around, so a watcher placed
|
|
8
|
-
* on one of them is unreliable. Instead
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
15
|
+
* on one of them is unreliable. Instead a few package.json files are polled,
|
|
16
|
+
* at the cost of a stat each: the packages known to be missing or outdated,
|
|
17
|
+
* and the packages the pages were served, whose package.json the url graph
|
|
18
|
+
* keeps (that is where "?v=" comes from). The project package.json is watched
|
|
19
|
+
* though: it is a single file, and editing it is what puts a dependency out of
|
|
20
|
+
* date in the first place.
|
|
21
|
+
*
|
|
22
|
+
* Both detections run in the same tick so that an install fixing an outdated
|
|
23
|
+
* package the page runs is reported once: two reports would be two reloads,
|
|
24
|
+
* the second one landing on the page that just came back.
|
|
13
25
|
*/
|
|
14
26
|
|
|
15
27
|
import { registerFileLifecycle } from "@jsenv/filesystem";
|
|
16
28
|
|
|
17
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
packageNameFromSpecifier,
|
|
31
|
+
readDependencyStatuses,
|
|
32
|
+
} from "../kitchen/package_dependencies.js";
|
|
18
33
|
|
|
19
34
|
const POLL_INTERVAL = 500;
|
|
20
35
|
|
|
21
36
|
export const watchDependencies = (
|
|
22
37
|
packageDirectory,
|
|
23
|
-
{
|
|
38
|
+
{
|
|
39
|
+
getKitchens = () => [],
|
|
40
|
+
onProblem,
|
|
41
|
+
onInstalled,
|
|
42
|
+
onChange,
|
|
43
|
+
pollInterval = POLL_INTERVAL,
|
|
44
|
+
},
|
|
24
45
|
) => {
|
|
25
46
|
let problemMap = new Map();
|
|
26
47
|
// every path given to the browser is relative to the package directory, the
|
|
@@ -35,11 +56,49 @@ export const watchDependencies = (
|
|
|
35
56
|
if (!packageDirectory.url) {
|
|
36
57
|
return watcher;
|
|
37
58
|
}
|
|
38
|
-
let timer = null;
|
|
39
59
|
|
|
40
|
-
const
|
|
60
|
+
const checkServedVersions = (installMap) => {
|
|
61
|
+
for (const kitchen of getKitchens()) {
|
|
62
|
+
for (const urlInfo of kitchen.graph.urlInfoMap.values()) {
|
|
63
|
+
const served = readServedVersion(urlInfo);
|
|
64
|
+
if (!served) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const installedPackageJson = readInstalledPackageJson(
|
|
68
|
+
packageDirectory,
|
|
69
|
+
served.directoryUrl,
|
|
70
|
+
);
|
|
71
|
+
if (!installedPackageJson) {
|
|
72
|
+
// the package is being rewritten, what it becomes is known once it is back
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const installedVersion = installedPackageJson.version;
|
|
76
|
+
if (installedVersion === served.version) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// the graph learns the file moved, as it would from a watcher: the
|
|
80
|
+
// files importing this package are cooked again with the new "?v="
|
|
81
|
+
// when the page comes back
|
|
82
|
+
urlInfo.onModified();
|
|
83
|
+
if (installMap.has(served.packageName)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
installMap.set(served.packageName, {
|
|
87
|
+
packageName: served.packageName,
|
|
88
|
+
installedVersion,
|
|
89
|
+
servedVersion: served.version,
|
|
90
|
+
declaredVersion: null,
|
|
91
|
+
severity: "warning",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const checkDeclared = (installMap) => {
|
|
98
|
+
const statusMap = new Map();
|
|
41
99
|
const nextProblemMap = new Map();
|
|
42
100
|
for (const status of readDependencyStatuses(packageDirectory)) {
|
|
101
|
+
statusMap.set(status.packageName, status);
|
|
43
102
|
if (status.state === "missing" || status.state === "outdated") {
|
|
44
103
|
nextProblemMap.set(status.packageName, status);
|
|
45
104
|
}
|
|
@@ -55,9 +114,21 @@ export const watchDependencies = (
|
|
|
55
114
|
}
|
|
56
115
|
}
|
|
57
116
|
for (const [packageName, previousStatus] of problemMap) {
|
|
58
|
-
if (
|
|
59
|
-
|
|
117
|
+
if (nextProblemMap.has(packageName)) {
|
|
118
|
+
continue;
|
|
60
119
|
}
|
|
120
|
+
const install = installMap.get(packageName);
|
|
121
|
+
if (install) {
|
|
122
|
+
install.declaredVersion = previousStatus.declaredVersion;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
installMap.set(packageName, {
|
|
126
|
+
packageName,
|
|
127
|
+
installedVersion: statusMap.get(packageName).installedVersion,
|
|
128
|
+
servedVersion: null,
|
|
129
|
+
declaredVersion: previousStatus.declaredVersion,
|
|
130
|
+
severity: previousStatus.severity,
|
|
131
|
+
});
|
|
61
132
|
}
|
|
62
133
|
const changed =
|
|
63
134
|
nextProblemMap.size !== problemMap.size ||
|
|
@@ -75,41 +146,92 @@ export const watchDependencies = (
|
|
|
75
146
|
if (changed) {
|
|
76
147
|
onChange(watcher.getProblems());
|
|
77
148
|
}
|
|
78
|
-
if (problemMap.size === 0) {
|
|
79
|
-
stopPolling();
|
|
80
|
-
} else {
|
|
81
|
-
startPolling();
|
|
82
|
-
}
|
|
83
149
|
};
|
|
84
150
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
151
|
+
// The declared dependencies are compared with node_modules only when
|
|
152
|
+
// something can have changed for them: the project package.json was edited,
|
|
153
|
+
// or a problem is known and an install may be fixing it. Comparing them at
|
|
154
|
+
// every tick would catch installs halfway and report a package as missing
|
|
155
|
+
// while it is being rewritten.
|
|
156
|
+
const check = ({ declared }) => {
|
|
157
|
+
const installMap = new Map();
|
|
158
|
+
checkServedVersions(installMap);
|
|
159
|
+
if (declared || problemMap.size > 0) {
|
|
160
|
+
checkDeclared(installMap);
|
|
88
161
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
};
|
|
92
|
-
const stopPolling = () => {
|
|
93
|
-
if (!timer) {
|
|
94
|
-
return;
|
|
162
|
+
for (const install of installMap.values()) {
|
|
163
|
+
onInstalled(install);
|
|
95
164
|
}
|
|
96
|
-
clearInterval(timer);
|
|
97
|
-
timer = null;
|
|
98
165
|
};
|
|
99
166
|
|
|
100
167
|
const unwatchPackageJson = registerFileLifecycle(
|
|
101
168
|
new URL("package.json", packageDirectory.url),
|
|
102
169
|
{
|
|
103
|
-
added: check,
|
|
104
|
-
updated: check,
|
|
170
|
+
added: () => check({ declared: true }),
|
|
171
|
+
updated: () => check({ declared: true }),
|
|
105
172
|
keepProcessAlive: false,
|
|
106
173
|
},
|
|
107
174
|
);
|
|
108
|
-
check();
|
|
175
|
+
check({ declared: true });
|
|
176
|
+
const timer = setInterval(() => {
|
|
177
|
+
check({ declared: false });
|
|
178
|
+
}, pollInterval);
|
|
179
|
+
timer.unref();
|
|
109
180
|
|
|
110
181
|
watcher.stop = () => {
|
|
111
|
-
|
|
182
|
+
clearInterval(timer);
|
|
112
183
|
unwatchPackageJson();
|
|
113
184
|
};
|
|
114
185
|
return watcher;
|
|
115
186
|
};
|
|
187
|
+
|
|
188
|
+
// The url graph keeps the content of every package.json it resolved a bare
|
|
189
|
+
// specifier against, and that content holds the version the page received.
|
|
190
|
+
// It is parsed once per content: the same string is seen at every tick.
|
|
191
|
+
const servedVersionCache = new WeakMap();
|
|
192
|
+
const readServedVersion = (urlInfo) => {
|
|
193
|
+
const { url, content } = urlInfo;
|
|
194
|
+
if (content === undefined) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (!url.startsWith("file:") || !url.endsWith("/package.json")) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const nodeModulesIndex = url.lastIndexOf("/node_modules/");
|
|
201
|
+
if (nodeModulesIndex === -1) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
// dereferenced url infos stay in the graph; nothing imports this package anymore
|
|
205
|
+
if (urlInfo.referenceFromOthersSet.size === 0) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const fromCache = servedVersionCache.get(urlInfo);
|
|
209
|
+
if (fromCache && fromCache.content === content) {
|
|
210
|
+
return fromCache;
|
|
211
|
+
}
|
|
212
|
+
let packageJson;
|
|
213
|
+
try {
|
|
214
|
+
packageJson = JSON.parse(content);
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const served = {
|
|
219
|
+
content,
|
|
220
|
+
directoryUrl: new URL("./", url).href,
|
|
221
|
+
packageName: packageNameFromSpecifier(
|
|
222
|
+
url.slice(nodeModulesIndex + "/node_modules/".length),
|
|
223
|
+
),
|
|
224
|
+
version: packageJson.version,
|
|
225
|
+
};
|
|
226
|
+
servedVersionCache.set(urlInfo, served);
|
|
227
|
+
return served;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// an install in progress can be caught halfway, with a package.json not written yet
|
|
231
|
+
const readInstalledPackageJson = (packageDirectory, directoryUrl) => {
|
|
232
|
+
try {
|
|
233
|
+
return packageDirectory.read(directoryUrl);
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* qui ne déclare pas le field side effect afin
|
|
8
|
-
* d'override le package.json du project qui lui dit qu'il ny en a pas
|
|
9
|
-
*
|
|
10
|
-
* On part du principe pour le moment que c'est la respo du package racine de déclarer cela
|
|
1
|
+
/*
|
|
2
|
+
* Tells the bundler which modules may be dropped when nothing imports a value
|
|
3
|
+
* from them, from the "sideEffects" field of the package.json closest to each
|
|
4
|
+
* file — a dependency's own declaration for its files, the project's for its
|
|
5
|
+
* own. A package that declares nothing keeps every module (the bundler's own
|
|
6
|
+
* default), so an undeclared root only loses precision, never code.
|
|
11
7
|
*
|
|
8
|
+
* Whether the project's package.json is written back with the side-effect
|
|
9
|
+
* files this build produced is decided in build.js, and only for a project that
|
|
10
|
+
* already declares the field; it is not this plugin's concern.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import { isSpecifierForNodeBuiltin } from "@jsenv/node-esm-resolution/src/node_builtin_specifiers.js";
|
|
@@ -18,15 +17,6 @@ export const jsenvPluginPackageSideEffects = ({ packageDirectory }) => {
|
|
|
18
17
|
if (!packageDirectory.url) {
|
|
19
18
|
return [];
|
|
20
19
|
}
|
|
21
|
-
const packageJson = packageDirectory.read(packageDirectory.url);
|
|
22
|
-
if (!packageJson) {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
const { sideEffects } = packageJson;
|
|
26
|
-
if (sideEffects !== false && !Array.isArray(sideEffects)) {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
20
|
const packageSideEffectsCacheMap = new Map();
|
|
31
21
|
const readSideEffectInfoFromClosestPackage = (urlInfo) => {
|
|
32
22
|
const closestPackageDirectoryUrl = urlInfo.packageDirectoryUrl;
|
|
@@ -4,7 +4,7 @@ events_manager: {
|
|
|
4
4
|
const callbacksMap = new Map();
|
|
5
5
|
let cleanup;
|
|
6
6
|
const addCallbacks = (namedCallbacks) => {
|
|
7
|
-
|
|
7
|
+
const sizeBeforeAdd = callbacksMap.size;
|
|
8
8
|
Object.keys(namedCallbacks).forEach((eventName) => {
|
|
9
9
|
const callback = namedCallbacks[eventName];
|
|
10
10
|
const existingCallbacks = callbacksMap.get(eventName);
|
|
@@ -17,7 +17,10 @@ events_manager: {
|
|
|
17
17
|
}
|
|
18
18
|
callbacks.push(callback);
|
|
19
19
|
});
|
|
20
|
-
|
|
20
|
+
// the effect owns whatever the callbacks need to exist (the connection),
|
|
21
|
+
// so it runs on the edge into "someone is listening" and its cleanup runs
|
|
22
|
+
// on the edge back out of it
|
|
23
|
+
if (sizeBeforeAdd === 0 && callbacksMap.size > 0) {
|
|
21
24
|
cleanup = effect();
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -25,7 +28,7 @@ events_manager: {
|
|
|
25
28
|
return () => {
|
|
26
29
|
if (removed) return;
|
|
27
30
|
removed = true;
|
|
28
|
-
|
|
31
|
+
const sizeBeforeRemove = callbacksMap.size;
|
|
29
32
|
Object.keys(namedCallbacks).forEach((eventName) => {
|
|
30
33
|
const callback = namedCallbacks[eventName];
|
|
31
34
|
const callbacks = callbacksMap.get(eventName);
|
|
@@ -40,12 +43,7 @@ events_manager: {
|
|
|
40
43
|
}
|
|
41
44
|
});
|
|
42
45
|
namedCallbacks = null; // allow garbage collect
|
|
43
|
-
if (
|
|
44
|
-
cleanup &&
|
|
45
|
-
typeof cleanup === "function" &&
|
|
46
|
-
callbacksMapSize > 0 &&
|
|
47
|
-
callbacksMapSize.size === 0
|
|
48
|
-
) {
|
|
46
|
+
if (cleanup && sizeBeforeRemove > 0 && callbacksMap.size === 0) {
|
|
49
47
|
cleanup();
|
|
50
48
|
cleanup = null;
|
|
51
49
|
}
|
|
@@ -86,9 +84,14 @@ connection_manager: {
|
|
|
86
84
|
CLOSED: "closed",
|
|
87
85
|
};
|
|
88
86
|
|
|
87
|
+
// The peer is a dev server being restarted, not a flaky network: there is no
|
|
88
|
+
// attempt count or time budget past which giving up would be correct. Attempts
|
|
89
|
+
// space out to retryAfterMax and never stop, and the moments where the server
|
|
90
|
+
// is most likely to be back already (tab visible, window focused, network
|
|
91
|
+
// back) skip the wait instead of leaving a detached page on screen.
|
|
89
92
|
createConnectionManager = (
|
|
90
93
|
attemptConnection,
|
|
91
|
-
{ logs, retry, retryAfter,
|
|
94
|
+
{ logs, retry, retryAfter, retryAfterMax },
|
|
92
95
|
) => {
|
|
93
96
|
const readyState = {
|
|
94
97
|
value: READY_STATES.CLOSED,
|
|
@@ -103,6 +106,7 @@ connection_manager: {
|
|
|
103
106
|
};
|
|
104
107
|
|
|
105
108
|
let _disconnect = () => {};
|
|
109
|
+
let _retryNow = () => {};
|
|
106
110
|
const connect = () => {
|
|
107
111
|
if (
|
|
108
112
|
readyState.value === READY_STATES.CONNECTING ||
|
|
@@ -111,11 +115,11 @@ connection_manager: {
|
|
|
111
115
|
return;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
let
|
|
115
|
-
let
|
|
118
|
+
let retryDelay = retryAfter;
|
|
119
|
+
let retryTimeout = null;
|
|
116
120
|
const attempt = () => {
|
|
121
|
+
retryTimeout = null;
|
|
117
122
|
readyState.goTo(READY_STATES.CONNECTING);
|
|
118
|
-
let timeout;
|
|
119
123
|
const cancelAttempt = attemptConnection({
|
|
120
124
|
onClosed: () => {
|
|
121
125
|
if (!retry) {
|
|
@@ -125,24 +129,6 @@ connection_manager: {
|
|
|
125
129
|
}
|
|
126
130
|
return;
|
|
127
131
|
}
|
|
128
|
-
if (retryCount > retryMaxAttempt) {
|
|
129
|
-
readyState.goTo(READY_STATES.CLOSED);
|
|
130
|
-
if (logs) {
|
|
131
|
-
console.info(
|
|
132
|
-
`[jsenv] could not connect to server after ${retryMaxAttempt} attempt`,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (retryAllocatedMs && msSpent > retryAllocatedMs) {
|
|
138
|
-
readyState.goTo(READY_STATES.CLOSED);
|
|
139
|
-
if (logs) {
|
|
140
|
-
console.info(
|
|
141
|
-
`[jsenv] could not connect to server in less than ${retryAllocatedMs}ms`,
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
132
|
// if closed while open -> connection lost
|
|
147
133
|
// otherwise it's the attempt to connect for the first time
|
|
148
134
|
// or to reconnect
|
|
@@ -153,13 +139,13 @@ connection_manager: {
|
|
|
153
139
|
);
|
|
154
140
|
}
|
|
155
141
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}, retryAfter);
|
|
142
|
+
retryTimeout = setTimeout(attempt, retryDelay);
|
|
143
|
+
const nextRetryDelay = retryDelay * 2;
|
|
144
|
+
retryDelay =
|
|
145
|
+
nextRetryDelay > retryAfterMax ? retryAfterMax : nextRetryDelay;
|
|
161
146
|
},
|
|
162
147
|
onOpen: () => {
|
|
148
|
+
retryDelay = retryAfter;
|
|
163
149
|
readyState.goTo(READY_STATES.OPEN);
|
|
164
150
|
if (logs) {
|
|
165
151
|
// console.info(`[jsenv] connected to server`);
|
|
@@ -168,10 +154,19 @@ connection_manager: {
|
|
|
168
154
|
});
|
|
169
155
|
_disconnect = () => {
|
|
170
156
|
cancelAttempt();
|
|
171
|
-
clearTimeout(
|
|
157
|
+
clearTimeout(retryTimeout);
|
|
158
|
+
retryTimeout = null;
|
|
172
159
|
readyState.goTo(READY_STATES.CLOSED);
|
|
173
160
|
};
|
|
174
161
|
};
|
|
162
|
+
_retryNow = () => {
|
|
163
|
+
if (retryTimeout === null) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
clearTimeout(retryTimeout);
|
|
167
|
+
retryDelay = retryAfter;
|
|
168
|
+
attempt();
|
|
169
|
+
};
|
|
175
170
|
attempt();
|
|
176
171
|
};
|
|
177
172
|
|
|
@@ -198,6 +193,9 @@ connection_manager: {
|
|
|
198
193
|
_disconnect();
|
|
199
194
|
}
|
|
200
195
|
});
|
|
196
|
+
const removeServerMightBeBackListeners = listenServerMightBeBack(() => {
|
|
197
|
+
_retryNow();
|
|
198
|
+
});
|
|
201
199
|
|
|
202
200
|
return {
|
|
203
201
|
readyState,
|
|
@@ -205,11 +203,31 @@ connection_manager: {
|
|
|
205
203
|
disconnect,
|
|
206
204
|
destroy: () => {
|
|
207
205
|
removePageUnloadListener();
|
|
206
|
+
removeServerMightBeBackListeners();
|
|
208
207
|
disconnect();
|
|
209
208
|
},
|
|
210
209
|
};
|
|
211
210
|
};
|
|
212
211
|
|
|
212
|
+
const listenServerMightBeBack = (callback) => {
|
|
213
|
+
const removeOnlineListener = listenEvent(window, "online", callback);
|
|
214
|
+
const removeFocusListener = listenEvent(window, "focus", callback);
|
|
215
|
+
const removeVisibilityChangeListener = listenEvent(
|
|
216
|
+
document,
|
|
217
|
+
"visibilitychange",
|
|
218
|
+
() => {
|
|
219
|
+
if (document.visibilityState === "visible") {
|
|
220
|
+
callback();
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
return () => {
|
|
225
|
+
removeOnlineListener();
|
|
226
|
+
removeFocusListener();
|
|
227
|
+
removeVisibilityChangeListener();
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
|
|
213
231
|
// const listenPageMightFreeze = (callback) => {
|
|
214
232
|
// const removePageHideListener = listenEvent(window, "pagehide", (pageHideEvent) => {
|
|
215
233
|
// if (pageHideEvent.persisted === true) {
|
|
@@ -275,8 +293,7 @@ connection_using_websocket: {
|
|
|
275
293
|
useEventsToManageConnection = true,
|
|
276
294
|
retry = false,
|
|
277
295
|
retryAfter = 1000,
|
|
278
|
-
|
|
279
|
-
retryAllocatedMs = Infinity,
|
|
296
|
+
retryAfterMax = 5000,
|
|
280
297
|
} = {},
|
|
281
298
|
) => {
|
|
282
299
|
const connectionManager = createConnectionManager(
|
|
@@ -319,7 +336,7 @@ connection_using_websocket: {
|
|
|
319
336
|
}
|
|
320
337
|
};
|
|
321
338
|
},
|
|
322
|
-
{ logs, retry, retryAfter,
|
|
339
|
+
{ logs, retry, retryAfter, retryAfterMax },
|
|
323
340
|
);
|
|
324
341
|
const eventsManager = createEventsManager({
|
|
325
342
|
effect: () => {
|
|
@@ -443,7 +460,6 @@ const serverEventsInterface = {
|
|
|
443
460
|
const websocketConnection = createWebSocketConnection(websocketUrl, {
|
|
444
461
|
logs,
|
|
445
462
|
retry: true,
|
|
446
|
-
retryAllocatedMs: 10_000,
|
|
447
463
|
});
|
|
448
464
|
|
|
449
465
|
const { readyState, connect, disconnect, listenEvents } =
|