@jsenv/core 41.5.20 → 41.5.22

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.
@@ -1,26 +1,47 @@
1
1
  /*
2
- * Detects when "npm install" makes a missing or outdated dependency match what
3
- * the project package.json declares, so the browser can be reloaded at that
4
- * moment.
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 the few packages known to be missing or
9
- * outdated are polled, which costs a couple of readFileSync and stops as soon as
10
- * they are all installed. The project package.json is watched though: it is a
11
- * single file, and editing it is what puts a dependency out of date in the first
12
- * place.
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 { readDependencyStatuses } from "../kitchen/package_dependencies.js";
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
- { onProblem, onInstalled, onChange, pollInterval = POLL_INTERVAL },
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 check = () => {
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 (!nextProblemMap.has(packageName)) {
59
- onInstalled(previousStatus);
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
- const startPolling = () => {
86
- if (timer) {
87
- return;
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
- timer = setInterval(check, pollInterval);
90
- timer.unref();
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
- stopPolling();
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
+ };
@@ -0,0 +1,137 @@
1
+ /*
2
+ * Text patches applied to files as they are served and built, keyed by file:
3
+ *
4
+ * patches: {
5
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
6
+ * }
7
+ *
8
+ * A key is a url pattern relative to the root directory ("./main.js",
9
+ * "**\/*.css"), or a path inside a package ("preact/dist/preact.mjs") found by
10
+ * walking up from the root directory into node_modules, the way node does,
11
+ * so the key holds wherever the package manager hoists the package.
12
+ *
13
+ * Every `from` must occur exactly once in the file, otherwise the file fails
14
+ * to cook and says which patch did not apply: a dependency update that moved
15
+ * the patched code must be looked at, never silently unpatched.
16
+ */
17
+
18
+ import { createMagicSource } from "@jsenv/sourcemap";
19
+ import { URL_META } from "@jsenv/url-meta";
20
+ import { asUrlWithoutSearch, urlToRelativeUrl } from "@jsenv/urls";
21
+ import { existsSync } from "node:fs";
22
+
23
+ export const jsenvPluginPatches = (rawPatches) => {
24
+ if (!rawPatches || Object.keys(rawPatches).length === 0) {
25
+ return [];
26
+ }
27
+ let findPatches;
28
+ const patchesPlugin = {
29
+ name: "jsenv:patches",
30
+ appliesDuring: "*",
31
+ init: (context) => {
32
+ const { rootDirectoryUrl } = context;
33
+ const patchesByPattern = {};
34
+ for (const key of Object.keys(rawPatches)) {
35
+ const patches = rawPatches[key];
36
+ assertPatches(patches, key);
37
+ patchesByPattern[resolvePatchKey(key, rootDirectoryUrl)] = patches;
38
+ }
39
+ const associations = URL_META.resolveAssociations(
40
+ { patches: patchesByPattern },
41
+ rootDirectoryUrl,
42
+ );
43
+ findPatches = (url) => {
44
+ const { patches } = URL_META.applyAssociations({
45
+ url: asUrlWithoutSearch(url),
46
+ associations,
47
+ });
48
+ return patches;
49
+ };
50
+ },
51
+ transformUrlContent: (urlInfo) => {
52
+ const patches = findPatches(urlInfo.url);
53
+ if (!patches) {
54
+ return null;
55
+ }
56
+ const { content } = urlInfo;
57
+ const magicSource = createMagicSource(content);
58
+ for (const { from, to } of patches) {
59
+ const start = content.indexOf(from);
60
+ const occurrenceCount =
61
+ start === -1 ? 0 : content.indexOf(from, start + 1) === -1 ? 1 : 2;
62
+ if (occurrenceCount !== 1) {
63
+ const fileRelativeUrl = urlToRelativeUrl(
64
+ urlInfo.url,
65
+ urlInfo.context.rootDirectoryUrl,
66
+ );
67
+ throw new Error(
68
+ `patch cannot apply on "${fileRelativeUrl}": ${JSON.stringify(from)} found ${occurrenceCount === 0 ? "nowhere" : "more than once"} in the file. The file may have changed since the patch was written.`,
69
+ );
70
+ }
71
+ magicSource.replace({
72
+ start,
73
+ end: start + from.length,
74
+ replacement: to,
75
+ });
76
+ }
77
+ return magicSource.toContentAndSourcemap();
78
+ },
79
+ };
80
+ return [patchesPlugin];
81
+ };
82
+
83
+ const assertPatches = (patches, key) => {
84
+ if (!Array.isArray(patches)) {
85
+ throw new TypeError(
86
+ `patches["${key}"] must be an array of { from, to }, got ${patches}`,
87
+ );
88
+ }
89
+ for (const patch of patches) {
90
+ if (
91
+ !patch ||
92
+ typeof patch.from !== "string" ||
93
+ patch.from === "" ||
94
+ typeof patch.to !== "string"
95
+ ) {
96
+ throw new TypeError(
97
+ `patches["${key}"] entries must be { from: string, to: string } with a non-empty "from"`,
98
+ );
99
+ }
100
+ }
101
+ };
102
+
103
+ // "./x", "../x", "/x", "file:///x" and "**/x" are url patterns; anything
104
+ // else names a path inside a package, looked up in node_modules
105
+ const resolvePatchKey = (key, rootDirectoryUrl) => {
106
+ if (
107
+ key.startsWith("./") ||
108
+ key.startsWith("../") ||
109
+ key.startsWith("/") ||
110
+ key.startsWith("file:") ||
111
+ key.startsWith("*")
112
+ ) {
113
+ return key;
114
+ }
115
+ const segments = key.split("/");
116
+ const packageName = key.startsWith("@")
117
+ ? `${segments[0]}/${segments[1]}`
118
+ : segments[0];
119
+ const pathInsidePackage = key.slice(packageName.length);
120
+ let directoryUrl = new URL(rootDirectoryUrl);
121
+ while (true) {
122
+ const packageDirectoryUrl = new URL(
123
+ `./node_modules/${packageName}/`,
124
+ directoryUrl,
125
+ );
126
+ if (existsSync(packageDirectoryUrl)) {
127
+ return String(new URL(`.${pathInsidePackage}`, packageDirectoryUrl));
128
+ }
129
+ const parentDirectoryUrl = new URL("../", directoryUrl);
130
+ if (parentDirectoryUrl.href === directoryUrl.href) {
131
+ throw new Error(
132
+ `patches["${key}"]: package "${packageName}" not found in any node_modules above ${rootDirectoryUrl}`,
133
+ );
134
+ }
135
+ directoryUrl = parentDirectoryUrl;
136
+ }
137
+ };
@@ -11,6 +11,7 @@ import { jsenvPluginProtocolFile } from "./protocol_file/jsenv_plugin_protocol_f
11
11
  import { jsenvPluginProtocolHttp } from "./protocol_http/jsenv_plugin_protocol_http.js";
12
12
  import { jsenvPluginDirectoryReferenceEffect } from "./directory_reference_effect/jsenv_plugin_directory_reference_effect.js";
13
13
  import { jsenvPluginInjections } from "./injections/jsenv_plugin_injections.js";
14
+ import { jsenvPluginPatches } from "./patches/jsenv_plugin_patches.js";
14
15
  import { jsenvPluginInlining } from "./inlining/jsenv_plugin_inlining.js";
15
16
  import { jsenvPluginCommonJsGlobals } from "./commonjs_globals/jsenv_plugin_commonjs_globals.js";
16
17
  import { jsenvPluginImportMetaScenarios } from "./import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js";
@@ -51,6 +52,7 @@ export const getCorePlugins = ({
51
52
  directoryListing = true,
52
53
  directoryReferenceEffect,
53
54
  supervisor,
55
+ patches,
54
56
  injections,
55
57
  transpilation = true,
56
58
  inlining = true,
@@ -92,6 +94,9 @@ export const getCorePlugins = ({
92
94
  ...(packageBundle
93
95
  ? [jsenvPluginWorkspaceBundle({ packageDirectory })]
94
96
  : []),
97
+ // before everything else: what the other plugins read must be the
98
+ // patched file
99
+ ...jsenvPluginPatches(patches),
95
100
  // before reference analysis: an url written by an injection must hold its
96
101
  // final value when references are analyzed
97
102
  jsenvPluginInjections(injections),
@@ -4,7 +4,7 @@ events_manager: {
4
4
  const callbacksMap = new Map();
5
5
  let cleanup;
6
6
  const addCallbacks = (namedCallbacks) => {
7
- let callbacksMapSize = callbacksMap.size;
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
- if (effect && callbacksMapSize === 0 && callbacksMapSize.size > 0) {
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
- callbacksMapSize = callbacksMap.size;
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, retryMaxAttempt, retryAllocatedMs },
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 retryCount = 0;
115
- let msSpent = 0;
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
- retryCount++;
157
- timeout = setTimeout(() => {
158
- msSpent += retryAfter;
159
- attempt();
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(timeout);
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
- retryMaxAttempt = Infinity,
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, retryMaxAttempt, retryAllocatedMs },
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 } =