@devioarts/capacitor-mdns 0.0.2 → 0.1.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 (38) hide show
  1. package/Package.swift +1 -1
  2. package/README.md +54 -67
  3. package/android/build.gradle +11 -11
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/java/com/devioarts/capacitor/mdns/mDNS.kt +217 -51
  6. package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +20 -9
  7. package/dist/docs.json +65 -3
  8. package/dist/esm/definitions.d.ts +28 -2
  9. package/dist/esm/definitions.js.map +1 -1
  10. package/dist/esm/index.js +1 -0
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/web.d.ts +3 -1
  13. package/dist/esm/web.js +12 -3
  14. package/dist/esm/web.js.map +1 -1
  15. package/dist/plugin.cjs.js +13 -3
  16. package/dist/plugin.cjs.js.map +1 -1
  17. package/dist/plugin.js +13 -3
  18. package/dist/plugin.js.map +1 -1
  19. package/docs/electron.md +65 -0
  20. package/docs/platform-setup.md +52 -0
  21. package/docs/playground.md +19 -0
  22. package/docs/runtime-behavior.md +14 -0
  23. package/electron/dist/plugin-settings.js +15 -0
  24. package/electron/dist/plugin-settings.js.map +1 -0
  25. package/electron/dist/plugin.cjs.js +344 -0
  26. package/electron/dist/plugin.cjs.js.map +1 -0
  27. package/electron/global.d.ts +2 -0
  28. package/electron/mdns-bridge.cjs +9 -4
  29. package/electron/mdns.ts +151 -63
  30. package/electron/rollup.config.mjs +22 -0
  31. package/electron/src/index.ts +1 -0
  32. package/electron/src/plugin-settings.ts +12 -0
  33. package/electron/tsconfig.json +21 -0
  34. package/electron/types-public/index.d.ts +25 -0
  35. package/ios/Sources/mDNSPlugin/mDNS.swift +95 -28
  36. package/ios/Sources/mDNSPlugin/mDNSPlugin.swift +12 -3
  37. package/ios/Tests/mDNSPluginTests/mDNSPluginTests.swift +3 -9
  38. package/package.json +47 -22
package/electron/mdns.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { Service, Browser } from 'bonjour-service';
2
1
  import Bonjour from 'bonjour-service';
3
2
  import { ipcMain } from 'electron';
4
3
 
@@ -7,9 +6,13 @@ import type {
7
6
  MdnsBroadcastResult,
8
7
  MdnsDiscoverOptions,
9
8
  MdnsDiscoverResult,
9
+ MdnsPluginPlatformResult,
10
10
  MdnsService,
11
11
  MdnsStopResult,
12
- } from '../src';
12
+ } from '../src/definitions';
13
+
14
+ type Service = InstanceType<typeof Bonjour.Service>;
15
+ type Browser = InstanceType<typeof Bonjour.Browser>;
13
16
 
14
17
  /**
15
18
  * Electron main-process implementation of the mDNS/Bonjour functionality.
@@ -24,6 +27,11 @@ import type {
24
27
  * - TXT records are forwarded as strings when present.
25
28
  */
26
29
  export class mDNS {
30
+ private readonly publishTimeoutMs = 5000;
31
+ private readonly stopTimeoutMs = 3000;
32
+ private broadcastQueue: Promise<unknown> = Promise.resolve();
33
+ private destroyed = false;
34
+
27
35
  //<editor-fold desc="Init/Destroy">
28
36
  private ipcRegistered = false;
29
37
  private registerIpc(): void {
@@ -32,12 +40,14 @@ export class mDNS {
32
40
  ipcMain.removeHandler('mdns:startBroadcast');
33
41
  ipcMain.removeHandler('mdns:stopBroadcast');
34
42
  ipcMain.removeHandler('mdns:discover');
43
+ ipcMain.removeHandler('mdns:getPluginPlatform');
35
44
  } catch {
36
45
  /* ignore */
37
46
  }
38
47
  ipcMain.handle('mdns:startBroadcast', (_evt, o: MdnsBroadcastOptions) => this.startBroadcast(o));
39
48
  ipcMain.handle('mdns:stopBroadcast', () => this.stopBroadcast());
40
49
  ipcMain.handle('mdns:discover', (_evt, o: MdnsDiscoverOptions) => this.discover(o));
50
+ ipcMain.handle('mdns:getPluginPlatform', () => this.getPluginPlatform());
41
51
  this.ipcRegistered = true;
42
52
  }
43
53
 
@@ -47,6 +57,7 @@ export class mDNS {
47
57
  ipcMain.removeHandler('mdns:startBroadcast');
48
58
  ipcMain.removeHandler('mdns:stopBroadcast');
49
59
  ipcMain.removeHandler('mdns:discover');
60
+ ipcMain.removeHandler('mdns:getPluginPlatform');
50
61
  } catch {
51
62
  /* ignore */
52
63
  }
@@ -58,13 +69,16 @@ export class mDNS {
58
69
  }
59
70
 
60
71
  async destroy(): Promise<void> {
72
+ this.destroyed = true;
61
73
  this.unregisterIpc();
62
- await this.stopBroadcast(); // no-op safe
63
- try {
64
- this.bonjour.destroy();
65
- } catch (err) {
66
- console.warn('[mDNS] bonjour.destroy error:', err);
67
- }
74
+ await this.withBroadcastLock(async () => {
75
+ await this.stopBroadcastUnlocked();
76
+ try {
77
+ this.bonjour.destroy();
78
+ } catch (err) {
79
+ console.warn('[mDNS] bonjour.destroy error:', err);
80
+ }
81
+ });
68
82
  }
69
83
 
70
84
  //</editor-fold>
@@ -76,6 +90,20 @@ export class mDNS {
76
90
  private toErr(err: unknown): string {
77
91
  return err instanceof Error ? err.message : String(err);
78
92
  }
93
+ private validatePort(port: unknown): string | null {
94
+ return Number.isInteger(port) && typeof port === 'number' && port > 0 && port <= 65535
95
+ ? null
96
+ : 'Missing/invalid port';
97
+ }
98
+ private isRecord(value: unknown): value is Record<string, unknown> {
99
+ return !!value && typeof value === 'object' && !Array.isArray(value);
100
+ }
101
+ private withBroadcastLock<T>(work: () => Promise<T>): Promise<T> {
102
+ // Publish and stop share one Bonjour service; serializing them prevents orphaned advertisers.
103
+ const run = this.broadcastQueue.catch(() => undefined).then(work);
104
+ this.broadcastQueue = run.catch(() => undefined);
105
+ return run;
106
+ }
79
107
 
80
108
  private normalize(name: string): string {
81
109
  return name.replace(/ \(\d+\)$/, '');
@@ -86,8 +114,9 @@ export class mDNS {
86
114
  t = this.normalize(target);
87
115
  return c === t || c.startsWith(t);
88
116
  }
89
- private parseType(typeWithDot?: string): { type: string; protocol: 'tcp' | 'udp' } {
90
- const s = (typeWithDot ?? '_http._tcp.').replace(/\.$/, '');
117
+ private parseType(typeWithDot?: unknown): { type: string; protocol: 'tcp' | 'udp' } {
118
+ const raw = typeof typeWithDot === 'string' && typeWithDot.trim() ? typeWithDot.trim() : '_http._tcp.';
119
+ const s = raw.replace(/\.$/, '');
91
120
  const m = /^_([^.]+)\._(tcp|udp)$/.exec(s);
92
121
  if (!m) return { type: 'http', protocol: 'tcp' };
93
122
  return { type: m[1], protocol: m[2] as 'tcp' | 'udp' };
@@ -103,11 +132,19 @@ export class mDNS {
103
132
  if (!svc || !this.hasCbStop(svc)) return;
104
133
  try {
105
134
  await new Promise<void>((res) => {
135
+ let settled = false;
136
+ const done = () => {
137
+ if (settled) return;
138
+ settled = true;
139
+ if (timeout) clearTimeout(timeout);
140
+ res();
141
+ };
142
+ const timeout = setTimeout(done, this.stopTimeoutMs);
106
143
  try {
107
- svc.stop(() => res());
144
+ svc.stop(done);
108
145
  } catch (e) {
109
146
  console.warn('[mDNS] service.stop threw:', e);
110
- res();
147
+ done();
111
148
  }
112
149
  });
113
150
  } catch (err) {
@@ -124,61 +161,93 @@ export class mDNS {
124
161
  }
125
162
 
126
163
  // ----------------------------- API -----------------------------
164
+ /**
165
+ * Return the Electron main-process implementation marker.
166
+ */
167
+ async getPluginPlatform(): Promise<MdnsPluginPlatformResult> {
168
+ return { platform: 'electron' };
169
+ }
170
+
127
171
  /**
128
172
  * Publish (advertise) a single Bonjour/mDNS service via bonjour-service.
129
173
  * @param options See MdnsBroadcastOptions for type/name/port/txt.
130
174
  * @returns Result indicating whether publishing is active and the final name.
131
175
  */
132
176
  async startBroadcast(options: MdnsBroadcastOptions): Promise<MdnsBroadcastResult> {
133
- const { type, protocol } = this.parseType(options.type);
134
- return new Promise<MdnsBroadcastResult>((resolve) => {
135
- let settled = false;
136
- const safeResolve = (r: MdnsBroadcastResult) => {
137
- if (!settled) {
138
- settled = true;
139
- resolve(r);
140
- }
141
- };
177
+ return this.withBroadcastLock(async () => {
178
+ const safeOptions = (options ?? {}) as Partial<MdnsBroadcastOptions>;
179
+ const portError = this.validatePort(safeOptions.port);
180
+ if (portError) return { publishing: false, name: '', error: true, errorMessage: portError };
181
+ if (this.destroyed)
182
+ return { publishing: false, name: '', error: true, errorMessage: 'mDNS instance is destroyed' };
142
183
 
143
- try {
144
- const svc = this.bonjour.publish({
145
- name: options.name || 'DevIOArtsMDNS',
146
- type,
147
- protocol,
148
- port: options.port,
149
- txt: options.txt,
150
- });
184
+ await this.stopBroadcastUnlocked();
151
185
 
152
- const onUp = () => {
153
- try {
154
- svc.removeListener('up', onUp);
155
- svc.removeListener('error', onError);
156
- } catch {
157
- /* ignore */
158
- }
159
- safeResolve({ publishing: true, name: svc.name || '', error: false, errorMessage: null });
160
- };
161
- const onError = (err: unknown) => {
162
- try {
163
- svc.removeListener('up', onUp);
164
- svc.removeListener('error', onError);
165
- } catch {
166
- /* ignore */
186
+ const { type, protocol } = this.parseType(safeOptions.type);
187
+ return new Promise<MdnsBroadcastResult>((resolve) => {
188
+ let settled = false;
189
+ let timeout: NodeJS.Timeout | null = null;
190
+ const safeResolve = (r: MdnsBroadcastResult) => {
191
+ if (!settled) {
192
+ settled = true;
193
+ if (timeout) {
194
+ clearTimeout(timeout);
195
+ timeout = null;
196
+ }
197
+ resolve(r);
167
198
  }
168
- try {
169
- if (this.hasCbStop(svc)) svc.stop();
170
- } catch (e) {
171
- console.warn('[mDNS] publish stop on error:', e);
172
- }
173
- safeResolve({ publishing: false, name: '', error: true, errorMessage: this.toErr(err) });
174
199
  };
175
200
 
176
- svc.once('up', onUp);
177
- svc.once('error', onError);
178
- this.advertiser = svc;
179
- } catch (e) {
180
- safeResolve({ publishing: false, name: '', error: true, errorMessage: this.toErr(e) });
181
- }
201
+ try {
202
+ const svc = this.bonjour.publish({
203
+ name:
204
+ typeof safeOptions.name === 'string' && safeOptions.name.trim()
205
+ ? safeOptions.name.trim()
206
+ : 'DevIOArtsMDNS',
207
+ type,
208
+ protocol,
209
+ port: safeOptions.port as number,
210
+ txt: this.isRecord(safeOptions.txt) ? this.normalizeTxt(safeOptions.txt) : undefined,
211
+ });
212
+
213
+ const cleanupListeners = () => {
214
+ try {
215
+ svc.removeListener('up', onUp);
216
+ svc.removeListener('error', onError);
217
+ } catch {
218
+ /* ignore */
219
+ }
220
+ };
221
+ const onUp = () => {
222
+ cleanupListeners();
223
+ safeResolve({ publishing: true, name: svc.name || '', error: false, errorMessage: null });
224
+ };
225
+ const onError = (err: unknown) => {
226
+ cleanupListeners();
227
+ void this.safeStopService(svc);
228
+ if (this.advertiser === svc) this.advertiser = undefined;
229
+ safeResolve({ publishing: false, name: '', error: true, errorMessage: this.toErr(err) });
230
+ };
231
+ const onTimeout = () => {
232
+ cleanupListeners();
233
+ void this.safeStopService(svc);
234
+ if (this.advertiser === svc) this.advertiser = undefined;
235
+ safeResolve({
236
+ publishing: false,
237
+ name: '',
238
+ error: true,
239
+ errorMessage: `Timed out waiting for service publish after ${this.publishTimeoutMs}ms`,
240
+ });
241
+ };
242
+
243
+ svc.once('up', onUp);
244
+ svc.once('error', onError);
245
+ timeout = setTimeout(onTimeout, this.publishTimeoutMs);
246
+ this.advertiser = svc;
247
+ } catch (e) {
248
+ safeResolve({ publishing: false, name: '', error: true, errorMessage: this.toErr(e) });
249
+ }
250
+ });
182
251
  });
183
252
  }
184
253
 
@@ -187,6 +256,10 @@ export class mDNS {
187
256
  * @returns Result indicating whether the advertiser is active and error info.
188
257
  */
189
258
  async stopBroadcast(): Promise<MdnsStopResult> {
259
+ return this.withBroadcastLock(() => this.stopBroadcastUnlocked());
260
+ }
261
+
262
+ private async stopBroadcastUnlocked(): Promise<MdnsStopResult> {
190
263
  try {
191
264
  if (this.advertiser) {
192
265
  await this.safeStopService(this.advertiser);
@@ -205,9 +278,14 @@ export class mDNS {
205
278
  * @param options See MdnsDiscoverOptions for type/name/timeout.
206
279
  */
207
280
  async discover(options: MdnsDiscoverOptions = {}): Promise<MdnsDiscoverResult> {
208
- const { type, protocol } = this.parseType(options.type);
209
- const targetId = options.name || null;
210
- const timeoutMs = options.timeout ?? 3000;
281
+ const safeOptions = (options ?? {}) as Partial<MdnsDiscoverOptions>;
282
+ if (this.destroyed) {
283
+ return { error: true, errorMessage: 'mDNS instance is destroyed', servicesFound: 0, services: [] };
284
+ }
285
+
286
+ const { type, protocol } = this.parseType(safeOptions.type);
287
+ const targetId = typeof safeOptions.name === 'string' && safeOptions.name ? safeOptions.name : null;
288
+ const timeoutMs = typeof safeOptions.timeout === 'number' ? safeOptions.timeout : 3000;
211
289
 
212
290
  let browser: Browser | null = null;
213
291
  const services: MdnsService[] = [];
@@ -216,8 +294,11 @@ export class mDNS {
216
294
 
217
295
  return new Promise<MdnsDiscoverResult>((resolve) => {
218
296
  let timer: NodeJS.Timeout | null = null;
297
+ let settled = false;
219
298
 
220
299
  const finish = () => {
300
+ if (settled) return;
301
+ settled = true;
221
302
  if (browser) {
222
303
  this.safeStopBrowser(browser);
223
304
  browser = null;
@@ -243,7 +324,7 @@ export class mDNS {
243
324
  domain: 'local.',
244
325
  port: s.port ?? 0,
245
326
  hosts: Array.isArray(s.addresses) ? s.addresses.slice() : [],
246
- txt: s.txt && Object.keys(s.txt).length ? (s.txt as Record<string, string>) : undefined,
327
+ txt: s.txt && Object.keys(s.txt).length ? this.normalizeTxt(s.txt) : undefined,
247
328
  };
248
329
  const key = `${item.name}:${item.port}`;
249
330
  if (!services.some((x) => `${x.name}:${x.port}` === key)) services.push(item);
@@ -251,12 +332,13 @@ export class mDNS {
251
332
  });
252
333
 
253
334
  // Record error and let the timeout conclude; results will still be returned.
254
- browser.on('error', (err: unknown) => {
335
+ const onBrowserError = (err: unknown) => {
255
336
  hadError = true;
256
337
  errMsg = this.toErr(err);
257
- });
338
+ };
339
+ (browser as any).on('error', onBrowserError);
258
340
 
259
- timer = setTimeout(finish, timeoutMs);
341
+ timer = setTimeout(finish, Math.max(0, timeoutMs));
260
342
  } catch (err) {
261
343
  hadError = true;
262
344
  errMsg = this.toErr(err);
@@ -264,4 +346,10 @@ export class mDNS {
264
346
  }
265
347
  });
266
348
  }
349
+
350
+ private normalizeTxt(txt: Record<string, unknown>): Record<string, string> {
351
+ return Object.fromEntries(
352
+ Object.entries(txt).map(([key, value]) => [key, Buffer.isBuffer(value) ? value.toString('utf8') : String(value)]),
353
+ );
354
+ }
267
355
  }
@@ -0,0 +1,22 @@
1
+ const external = ['bonjour-service', 'electron'];
2
+
3
+ export default [
4
+ {
5
+ input: 'electron/build/electron/src/index.js',
6
+ output: {
7
+ file: 'electron/dist/plugin.cjs.js',
8
+ format: 'cjs',
9
+ sourcemap: true,
10
+ },
11
+ external,
12
+ },
13
+ {
14
+ input: 'electron/build/electron/src/plugin-settings.js',
15
+ output: {
16
+ file: 'electron/dist/plugin-settings.js',
17
+ format: 'cjs',
18
+ sourcemap: true,
19
+ },
20
+ external,
21
+ },
22
+ ];
@@ -0,0 +1 @@
1
+ export { mDNS } from '../mdns';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Metadata consumed by the capacitor-electron runtime generator (`npm run update`).
3
+ *
4
+ * Import path for the JS layer: `@devioarts/capacitor-mdns/electron/settings`.
5
+ */
6
+ export const pluginSettings = {
7
+ pluginClass: 'mDNS',
8
+ pluginMethods: ['getPluginPlatform', 'startBroadcast', 'stopBroadcast', 'discover'] as const,
9
+ pluginEvents: [] as const,
10
+ } as const;
11
+
12
+ export type PluginSettings = typeof pluginSettings;
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowSyntheticDefaultImports": true,
4
+ "declaration": true,
5
+ "esModuleInterop": true,
6
+ "lib": ["es2020"],
7
+ "module": "esnext",
8
+ "moduleResolution": "bundler",
9
+ "noImplicitAny": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true,
12
+ "outDir": "build",
13
+ "rootDir": "..",
14
+ "sourceMap": true,
15
+ "skipLibCheck": true,
16
+ "strict": true,
17
+ "target": "ES2017",
18
+ "types": ["node"]
19
+ },
20
+ "include": ["src/**/*"]
21
+ }
@@ -0,0 +1,25 @@
1
+ import type {
2
+ MdnsBroadcastOptions,
3
+ MdnsBroadcastResult,
4
+ MdnsDiscoverOptions,
5
+ MdnsDiscoverResult,
6
+ MdnsPluginPlatformResult,
7
+ MdnsStopResult,
8
+ } from '../../dist/esm/definitions';
9
+
10
+ export declare class mDNS {
11
+ getPluginPlatform(): Promise<MdnsPluginPlatformResult>;
12
+ startBroadcast(options: MdnsBroadcastOptions): Promise<MdnsBroadcastResult>;
13
+ stopBroadcast(): Promise<MdnsStopResult>;
14
+ discover(options?: MdnsDiscoverOptions): Promise<MdnsDiscoverResult>;
15
+ init(): void;
16
+ destroy(): Promise<void>;
17
+ }
18
+
19
+ export declare const pluginSettings: {
20
+ readonly pluginClass: 'mDNS';
21
+ readonly pluginMethods: readonly ['getPluginPlatform', 'startBroadcast', 'stopBroadcast', 'discover'];
22
+ readonly pluginEvents: readonly [];
23
+ };
24
+
25
+ export type PluginSettings = typeof pluginSettings;