@bleedingdev/modern-js-federation-runtime 3.9.3-ultramodern.8 → 3.9.3-ultramodern.9

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 (25) hide show
  1. package/dist/cjs/module-federation/native-browser-dev-client.js +320 -0
  2. package/dist/cjs/module-federation/native-browser-dev-refresh.js +226 -0
  3. package/dist/cjs/module-federation/native-browser-dev-runtime-plugin.js +233 -0
  4. package/dist/cjs/module-federation/native-dev-refresh-runtime-plugin.js +267 -0
  5. package/dist/cjs/module-federation/renderer-runtime-plugin.js +5 -2
  6. package/dist/esm/module-federation/native-browser-dev-client.mjs +276 -0
  7. package/dist/esm/module-federation/native-browser-dev-refresh.mjs +182 -0
  8. package/dist/esm/module-federation/native-browser-dev-runtime-plugin.mjs +183 -0
  9. package/dist/esm/module-federation/native-dev-refresh-runtime-plugin.mjs +229 -0
  10. package/dist/esm/module-federation/renderer-runtime-plugin.mjs +5 -2
  11. package/dist/esm-node/module-federation/native-browser-dev-client.mjs +277 -0
  12. package/dist/esm-node/module-federation/native-browser-dev-refresh.mjs +183 -0
  13. package/dist/esm-node/module-federation/native-browser-dev-runtime-plugin.mjs +184 -0
  14. package/dist/esm-node/module-federation/native-dev-refresh-runtime-plugin.mjs +230 -0
  15. package/dist/esm-node/module-federation/renderer-runtime-plugin.mjs +5 -2
  16. package/dist/types/module-federation/native-browser-dev-client.d.ts +41 -0
  17. package/dist/types/module-federation/native-browser-dev-refresh.d.ts +86 -0
  18. package/dist/types/module-federation/native-browser-dev-runtime-plugin.d.ts +40 -0
  19. package/dist/types/module-federation/native-dev-refresh-runtime-plugin.d.ts +90 -0
  20. package/package.json +54 -7
  21. package/src/module-federation/native-browser-dev-client.ts +424 -0
  22. package/src/module-federation/native-browser-dev-refresh.ts +373 -0
  23. package/src/module-federation/native-browser-dev-runtime-plugin.ts +276 -0
  24. package/src/module-federation/native-dev-refresh-runtime-plugin.ts +445 -0
  25. package/src/module-federation/renderer-runtime-plugin.ts +7 -2
@@ -0,0 +1,229 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const LOAD_WITH_CAPTURE = Symbol.for('ultramodern.federation.load-with-capture');
3
+ function nativeDevRefreshRuntimePlugin(options) {
4
+ if (!options.revisionHeader || !Number.isSafeInteger(options.requestTimeout) || options.requestTimeout <= 0) throw new TypeError('Native federation dev refresh requires a bounded revision probe.');
5
+ const hosts = new WeakMap();
6
+ const calls = new AsyncLocalStorage();
7
+ const probe = (host, lane, expose)=>{
8
+ if (lane.probe) return lane.probe;
9
+ const info = lane.info;
10
+ if (!info) throw new Error('Native federation dev remote has no admitted entry.');
11
+ const pending = (async ()=>{
12
+ const init = {
13
+ method: 'HEAD',
14
+ cache: 'no-store',
15
+ signal: AbortSignal.timeout(options.requestTimeout)
16
+ };
17
+ let clearDeadline;
18
+ const signal = init.signal;
19
+ const deadline = new Promise((_resolve, reject)=>{
20
+ const abort = ()=>reject(signal.reason);
21
+ signal.addEventListener('abort', abort, {
22
+ once: true
23
+ });
24
+ clearDeadline = ()=>signal.removeEventListener('abort', abort);
25
+ });
26
+ const request = (async ()=>{
27
+ const hooked = await host.loaderHook.lifecycle.fetch.emit(info.entry, init, info, {
28
+ initiator: 'loadRemote',
29
+ id: expose && '.' !== expose ? `${info.name}/${expose.replace(/^\.\//u, '')}` : info.name,
30
+ resourceType: 'remoteEntry',
31
+ url: info.entry,
32
+ expose
33
+ });
34
+ const response = hooked instanceof Response ? hooked : await globalThis.fetch(info.entry, init);
35
+ if (signal.aborted) {
36
+ response.body?.cancel().catch(()=>{});
37
+ throw signal.reason;
38
+ }
39
+ return response;
40
+ })();
41
+ let response;
42
+ try {
43
+ response = await Promise.race([
44
+ request,
45
+ deadline
46
+ ]);
47
+ } finally{
48
+ clearDeadline();
49
+ }
50
+ response.body?.cancel().catch(()=>{});
51
+ if (!response.ok) throw Object.assign(new Error(`Native federation dev revision request failed: ${response.status} ${info.entry}`), {
52
+ status: response.status
53
+ });
54
+ const revision = response.headers.get(options.revisionHeader);
55
+ if (null !== revision && !/^[a-f0-9]{64}$/u.test(revision)) throw new Error('Native federation dev remote returned an invalid graph revision.');
56
+ if (null === revision && 'string' == typeof lane.revision) throw new Error('Native federation dev remote lost its completed graph revision.');
57
+ return revision;
58
+ })();
59
+ lane.probe = pending;
60
+ const clear = ()=>{
61
+ if (lane.probe === pending) lane.probe = void 0;
62
+ };
63
+ pending.then(clear, clear);
64
+ return pending;
65
+ };
66
+ return {
67
+ name: 'ultramodern-native-federation-dev-refresh',
68
+ beforeInit (args) {
69
+ const host = args.origin;
70
+ if (hosts.has(host)) return args;
71
+ const lanes = new Map(options.remotes.map((remote)=>[
72
+ remote.name,
73
+ {
74
+ remote: {
75
+ ...remote
76
+ },
77
+ active: new Set()
78
+ }
79
+ ]));
80
+ hosts.set(host, lanes);
81
+ const load = host.loadRemote.bind(host);
82
+ const loadWithCapture = async (id, loadOptions, capture)=>{
83
+ const parent = calls.getStore();
84
+ const lease = {
85
+ host,
86
+ ...parent?.host === host ? {
87
+ parent
88
+ } : {}
89
+ };
90
+ return calls.run(lease, async ()=>{
91
+ let success = false;
92
+ try {
93
+ const module = await load(id, loadOptions);
94
+ const captured = capture?.(module, lease.resolution);
95
+ success = null !== module && true === lease.admitted;
96
+ return {
97
+ module,
98
+ captured
99
+ };
100
+ } finally{
101
+ lease.active = false;
102
+ lease.release?.(success);
103
+ }
104
+ });
105
+ };
106
+ host.loadRemote = async (id, loadOptions)=>(await loadWithCapture(id, loadOptions)).module;
107
+ host[LOAD_WITH_CAPTURE] = (id, capture)=>loadWithCapture(id, void 0, capture);
108
+ return args;
109
+ },
110
+ async beforeRequest (args) {
111
+ const lease = calls.getStore();
112
+ if (!lease || lease.host !== args.origin) return args;
113
+ const lane = [
114
+ ...hosts.get(args.origin)?.values() ?? []
115
+ ].find(({ remote })=>[
116
+ remote.alias,
117
+ remote.name
118
+ ].some((name)=>args.id === name || args.id.startsWith(`${name}/`)));
119
+ if (!lane || lease.lane) return args;
120
+ lease.lane = lane;
121
+ const prefix = [
122
+ lane.remote.alias,
123
+ lane.remote.name
124
+ ].find((name)=>args.id === name || args.id.startsWith(`${name}/`));
125
+ const expose = args.id === prefix ? '.' : `./${args.id.slice(prefix.length + 1)}`;
126
+ for(let ancestor = lease.parent; ancestor; ancestor = ancestor.parent)if (ancestor.lane === lane && ancestor.active) {
127
+ let release;
128
+ const finished = new Promise((resolve)=>{
129
+ release = resolve;
130
+ });
131
+ lane.active.add(finished);
132
+ lease.active = true;
133
+ lease.snapshot = ancestor.snapshot;
134
+ lease.release = ()=>{
135
+ lane.active.delete(finished);
136
+ release();
137
+ };
138
+ return args;
139
+ }
140
+ for(;;){
141
+ if (lane.transition) {
142
+ await lane.transition;
143
+ continue;
144
+ }
145
+ const revision = void 0 === lane.revision ? void 0 : null === lane.revision ? null : await probe(args.origin, lane, expose);
146
+ if (lane.transition) {
147
+ await lane.transition;
148
+ continue;
149
+ }
150
+ let finish;
151
+ const finished = new Promise((resolve)=>{
152
+ finish = resolve;
153
+ });
154
+ if (void 0 === lane.revision || revision !== lane.revision) {
155
+ lane.transition = finished;
156
+ lease.release = (success)=>{
157
+ if (success) lane.revision = lane.candidate ?? null;
158
+ if (lane.transition === finished) lane.transition = void 0;
159
+ finish();
160
+ };
161
+ while(lane.active.size)await Promise.allSettled([
162
+ ...lane.active
163
+ ]);
164
+ lane.candidate = revision;
165
+ if (void 0 !== lane.revision) {
166
+ lease.active = true;
167
+ lease.snapshot = lane.snapshot;
168
+ const retained = new Set();
169
+ for (const scope of Object.values(args.origin.shareScopeMap ?? {}))for (const [pkgName, versions] of Object.entries(scope))if (options.shared?.some((key)=>key === pkgName || key.endsWith('/') && pkgName.startsWith(key)) && Object.values(versions).some((shared)=>shared.loaded || shared.loading || shared.lib)) retained.add(pkgName);
170
+ for (const pkgName of retained){
171
+ if (!args.origin.loadShare) throw new TypeError('Native federation dev refresh requires the host share lifecycle.');
172
+ await args.origin.loadShare(pkgName);
173
+ }
174
+ while(lane.active.size)await Promise.allSettled([
175
+ ...lane.active
176
+ ]);
177
+ args.origin.registerRemotes([
178
+ {
179
+ ...lane.remote
180
+ }
181
+ ], {
182
+ force: true
183
+ });
184
+ lease.snapshot = void 0;
185
+ }
186
+ } else {
187
+ lane.active.add(finished);
188
+ lease.release = ()=>{
189
+ lane.active.delete(finished);
190
+ finish();
191
+ };
192
+ }
193
+ lease.active = true;
194
+ return args;
195
+ }
196
+ },
197
+ loadSnapshot (args) {
198
+ const lease = calls.getStore();
199
+ if (!lease || args.options !== lease.host.options || args.moduleInfo.name !== lease.lane?.remote.name) return args;
200
+ if (void 0 !== lease.snapshot) return {
201
+ ...args,
202
+ remoteSnapshot: lease.snapshot
203
+ };
204
+ lease.snapshot = args.remoteSnapshot;
205
+ return args;
206
+ },
207
+ afterLoadSnapshot (args) {
208
+ const lease = calls.getStore();
209
+ if (lease?.host === args.host && args.moduleInfo.name === lease.lane?.remote.name) lease.snapshot = args.remoteSnapshot;
210
+ return args;
211
+ },
212
+ async afterResolve (args) {
213
+ const lane = hosts.get(args.origin)?.get(args.remote.name);
214
+ const lease = calls.getStore();
215
+ if (!lane || lease?.host !== args.origin || lease.lane !== lane || !lease.active) return args;
216
+ if (lane.info?.entry !== args.remoteInfo.entry) lane.candidate = void 0;
217
+ lane.info = args.remoteInfo;
218
+ if (void 0 === lane.candidate) lane.candidate = await probe(args.origin, lane, args.expose);
219
+ lease.admitted = true;
220
+ lane.snapshot = args.remoteSnapshot;
221
+ lease.resolution = {
222
+ remoteSnapshot: args.remoteSnapshot,
223
+ expose: args.expose
224
+ };
225
+ return args;
226
+ }
227
+ };
228
+ }
229
+ export default nativeDevRefreshRuntimePlugin;
@@ -109,6 +109,9 @@ function createRendererFederationRuntimePlugin(options) {
109
109
  const observedEntries = new WeakMap();
110
110
  const acceptedEntries = new Set();
111
111
  const versions = rendererShareVersions(expected);
112
+ const sharedVersion = (name)=>versions.get(name) ?? ('react' === expected.profile.renderer ? void 0 : [
113
+ ...versions
114
+ ].find(([owner])=>name.startsWith(`${owner}/`))?.[1]);
112
115
  return {
113
116
  name: 'ultramodern-renderer-federation-contract',
114
117
  beforeRegisterRemote (args) {
@@ -182,7 +185,7 @@ function createRendererFederationRuntimePlugin(options) {
182
185
  return args;
183
186
  },
184
187
  resolveShare (args) {
185
- const version = versions.get(args.pkgName);
188
+ const version = sharedVersion(args.pkgName);
186
189
  if (void 0 === version) return args;
187
190
  const resolve = args.resolver;
188
191
  return {
@@ -207,7 +210,7 @@ function createRendererFederationRuntimePlugin(options) {
207
210
  };
208
211
  },
209
212
  afterLoadShare (args) {
210
- if (versions.has(args.pkgName) && args.selectedShared) {
213
+ if (void 0 !== sharedVersion(args.pkgName) && args.selectedShared) {
211
214
  const shared = record(args.selectedShared.treeShaking) && args.selectedShared.treeShaking.loaded ? args.selectedShared.treeShaking : args.selectedShared;
212
215
  selected.set(args.pkgName, Object.freeze({
213
216
  lib: shared.lib,
@@ -0,0 +1,277 @@
1
+ import "node:module";
2
+ import { isNativeBrowserDevCommitError } from "./native-browser-dev-refresh.mjs";
3
+ import { hasNativeBrowserDevPublisher, readNativeBrowserDevPublication, replaceNativeBrowserDevRemote } from "./native-browser-dev-runtime-plugin.mjs";
4
+ const CLIENTS = Symbol.for('ultramodern.federation.browser-dev-clients.v1');
5
+ const clients = ()=>{
6
+ const root = globalThis;
7
+ if (!root[CLIENTS]) Object.defineProperty(root, CLIENTS, {
8
+ value: new Map()
9
+ });
10
+ return root[CLIENTS];
11
+ };
12
+ function commitClient(remoteName, graph) {
13
+ const state = clients().get(remoteName);
14
+ const next = state?.candidates.get(graph.runtimeHash)?.deref();
15
+ if (!state || !next) throw new Error('Accepted native graph has no matching fresh HMR runtime.');
16
+ if (state.active === next) return;
17
+ state.active?.stop();
18
+ state.active = next;
19
+ next.start();
20
+ }
21
+ function prepareNativeBrowserDevClient(remoteName, manifest) {
22
+ const hash = /\/__ultramodern_mf__\/([a-f0-9]+)\//u.exec(new URL(manifest).pathname)?.[1];
23
+ const candidate = hash && clients().get(remoteName)?.candidates.get(hash)?.deref();
24
+ if (!candidate) throw new Error('Native graph has no loaded standby HMR runtime.');
25
+ return candidate.lease();
26
+ }
27
+ function releaseNativeBrowserDevClient(remoteName, manifest) {
28
+ const hash = /\/__ultramodern_mf__\/([a-f0-9]+)\//u.exec(new URL(manifest).pathname)?.[1];
29
+ if (!hash) return;
30
+ const state = clients().get(remoteName);
31
+ const candidate = state?.candidates.get(hash)?.deref();
32
+ if (!state || !candidate) return;
33
+ candidate.releaseStandby();
34
+ }
35
+ function startNativeBrowserDevClient(options) {
36
+ const state = clients().get(options.remoteName) ?? {
37
+ candidates: new Map()
38
+ };
39
+ clients().set(options.remoteName, state);
40
+ const initialHash = options.hash();
41
+ let socket;
42
+ let ping;
43
+ let reconnect;
44
+ let reconnectCount = 0;
45
+ let lastHash;
46
+ let running = false;
47
+ let applying;
48
+ let lifetime;
49
+ let removeCustom;
50
+ const custom = new Map();
51
+ const queued = [];
52
+ const leases = new Set();
53
+ const base = options.serverBase.endsWith('/') ? options.serverBase : `${options.serverBase}/`;
54
+ const report = (error)=>{
55
+ console.error('[ultramodern] Native federation development update failed:', error);
56
+ if (false !== options.config.overlay && ('object' != typeof options.config.overlay || false !== options.config.overlay.errors)) options.overlay()?.create('Native federation update failed', String(error));
57
+ };
58
+ const send = (message)=>{
59
+ if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message));
60
+ else queued.push(message);
61
+ };
62
+ const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
63
+ const onError = (event)=>send({
64
+ type: 'client-error',
65
+ id: crypto.randomUUID(),
66
+ message: event.message,
67
+ name: event.error?.name,
68
+ stack: event.error?.stack
69
+ });
70
+ const onRejected = (event)=>send({
71
+ type: 'client-error',
72
+ id: crypto.randomUUID(),
73
+ message: `Uncaught (in promise) ${errorMessage(event.reason)}`,
74
+ name: event.reason?.name,
75
+ stack: event.reason?.stack
76
+ });
77
+ const fullReload = (data)=>{
78
+ if (!options.liveReload) return;
79
+ if (data?.path?.endsWith('.html')) {
80
+ const pathname = decodeURI(location.pathname);
81
+ const target = base + data.path.slice(1);
82
+ if (pathname !== target && pathname !== target.slice(0, -5) && !(pathname.endsWith('/') && `${pathname}index.html` === target)) return;
83
+ }
84
+ location.reload();
85
+ };
86
+ const apply = ()=>{
87
+ if (!running || applying || !lastHash || lastHash === options.hash()) return;
88
+ const signal = lifetime.signal;
89
+ const targetHash = lastHash;
90
+ applying = (async ()=>{
91
+ if (hasNativeBrowserDevPublisher(options.remoteName)) {
92
+ const graph = await readNativeBrowserDevPublication(options.remoteName, {
93
+ signal: AbortSignal.any([
94
+ signal,
95
+ AbortSignal.timeout(3000)
96
+ ]),
97
+ revisionHeader: options.revisionHeader
98
+ });
99
+ if (signal.aborted) throw signal.reason;
100
+ if (graph.buildId !== options.buildId) {
101
+ try {
102
+ await replaceNativeBrowserDevRemote(graph);
103
+ } catch (error) {
104
+ if (running && !signal.aborted && hasNativeBrowserDevPublisher(options.remoteName) && isNativeBrowserDevCommitError(error, graph.revision)) {
105
+ state.failure = {
106
+ runtimeHash: graph.runtimeHash,
107
+ error
108
+ };
109
+ report(error);
110
+ commitClient(options.remoteName, graph);
111
+ }
112
+ throw error;
113
+ }
114
+ if (!running || signal.aborted || !hasNativeBrowserDevPublisher(options.remoteName)) return;
115
+ commitClient(options.remoteName, graph);
116
+ return;
117
+ }
118
+ }
119
+ if (!options.hot) return fullReload();
120
+ if ('idle' !== options.hot.status()) return;
121
+ try {
122
+ const updated = await options.hot.check(true);
123
+ if (!updated) fullReload();
124
+ } catch (error) {
125
+ console.error('[rsbuild] HMR update failed, performing full reload:', error);
126
+ fullReload();
127
+ }
128
+ })().catch((error)=>{
129
+ if (!signal.aborted) report(error);
130
+ }).finally(()=>{
131
+ applying = void 0;
132
+ if (running && !signal.aborted && lastHash !== targetHash) apply();
133
+ });
134
+ };
135
+ const clearOverlayForBuild = ()=>{
136
+ if (state.failure) {
137
+ if (!lastHash || lastHash === state.failure.runtimeHash) return;
138
+ state.failure = void 0;
139
+ }
140
+ options.overlay()?.clear();
141
+ };
142
+ const message = (event)=>{
143
+ let value;
144
+ try {
145
+ value = JSON.parse(event.data);
146
+ } catch (error) {
147
+ report(error);
148
+ return;
149
+ }
150
+ switch(value.type){
151
+ case 'hash':
152
+ lastHash = value.data;
153
+ break;
154
+ case 'ok':
155
+ clearOverlayForBuild();
156
+ apply();
157
+ break;
158
+ case 'warnings':
159
+ for (const warning of value.data?.text ?? [])console.warn(warning);
160
+ clearOverlayForBuild();
161
+ apply();
162
+ break;
163
+ case 'errors':
164
+ for (const error of value.data?.text ?? [])console.error(error);
165
+ if (value.data?.html) options.overlay()?.create('Build failed', value.data.html);
166
+ break;
167
+ case 'full-reload':
168
+ fullReload(value.data);
169
+ break;
170
+ case 'static-changed':
171
+ fullReload();
172
+ break;
173
+ case 'custom':
174
+ for (const listener of custom.get(value.data?.event) ?? [])listener(value.data.data);
175
+ break;
176
+ case 'resolved-client-error':
177
+ options.overlay()?.create('Runtime errors', String(value.data?.message ?? ''));
178
+ break;
179
+ }
180
+ };
181
+ const socketUrl = (fallback)=>{
182
+ const url = new URL('http://localhost');
183
+ url.hostname = (fallback ? options.serverHost : options.config.host) || location.hostname;
184
+ url.port = String((fallback ? options.serverPort : options.config.port) || location.port);
185
+ url.protocol = options.config.protocol || ('https:' === location.protocol ? 'wss:' : 'ws:');
186
+ url.pathname = options.config.path;
187
+ url.searchParams.set('token', options.token);
188
+ return options.resolveWebSocketUrl?.(url.href) ?? url.href;
189
+ };
190
+ const detachSocket = ()=>{
191
+ clearInterval(ping);
192
+ ping = void 0;
193
+ const previous = socket;
194
+ socket = void 0;
195
+ if (!previous) return;
196
+ previous.onopen = previous.onclose = previous.onmessage = previous.onerror = null;
197
+ previous.close();
198
+ };
199
+ const connect = (fallback = false)=>{
200
+ if (!running) return;
201
+ socket = new WebSocket(socketUrl(fallback));
202
+ socket.onopen = ()=>{
203
+ reconnectCount = 0;
204
+ ping = setInterval(()=>send({
205
+ type: 'ping'
206
+ }), 30000);
207
+ for (const item of queued.splice(0))send(item);
208
+ };
209
+ socket.onmessage = message;
210
+ socket.onclose = ()=>{
211
+ detachSocket();
212
+ if (!running || reconnectCount >= options.config.reconnect) return;
213
+ reconnectCount++;
214
+ reconnect = setTimeout(()=>connect(), 1000 * 1.5 ** reconnectCount);
215
+ };
216
+ socket.onerror = ()=>{
217
+ if (options.resolveWebSocketUrl || fallback || socketUrl(false) === socketUrl(true)) return;
218
+ detachSocket();
219
+ connect(true);
220
+ };
221
+ };
222
+ const client = {
223
+ arm () {
224
+ removeCustom ??= options.installCustomListeners(custom);
225
+ },
226
+ lease () {
227
+ const lease = {};
228
+ leases.add(lease);
229
+ client.arm();
230
+ return ()=>{
231
+ if (!leases.delete(lease)) return;
232
+ client.releaseStandby();
233
+ };
234
+ },
235
+ releaseStandby () {
236
+ if (state.active !== client && !leases.size) client.stop();
237
+ },
238
+ start () {
239
+ if (running) return;
240
+ running = true;
241
+ lifetime = new AbortController();
242
+ client.arm();
243
+ if (options.browserLogs) {
244
+ window.addEventListener('error', onError);
245
+ window.addEventListener('unhandledrejection', onRejected);
246
+ }
247
+ connect();
248
+ },
249
+ stop () {
250
+ if (!running && !removeCustom) return;
251
+ running = false;
252
+ lifetime?.abort(new Error('Native browser HMR runtime retired.'));
253
+ clearTimeout(reconnect);
254
+ reconnect = void 0;
255
+ detachSocket();
256
+ window.removeEventListener('error', onError);
257
+ window.removeEventListener('unhandledrejection', onRejected);
258
+ removeCustom?.();
259
+ removeCustom = void 0;
260
+ custom.clear();
261
+ queued.length = 0;
262
+ }
263
+ };
264
+ client.arm();
265
+ state.candidates.set(initialHash, new WeakRef(client));
266
+ if (!state.active) {
267
+ state.active = client;
268
+ client.start();
269
+ }
270
+ return ()=>{
271
+ client.stop();
272
+ if (state.active === client) state.active = void 0;
273
+ if (state.candidates.get(initialHash)?.deref() === client) state.candidates.delete(initialHash);
274
+ if (!state.active && !state.candidates.size) clients().delete(options.remoteName);
275
+ };
276
+ }
277
+ export { prepareNativeBrowserDevClient, releaseNativeBrowserDevClient, startNativeBrowserDevClient };