diff --git a/packages/general/src/util/Mutex.ts b/packages/general/src/util/Mutex.ts index 0fd5f3241..67b81cd66 100644 --- a/packages/general/src/util/Mutex.ts +++ b/packages/general/src/util/Mutex.ts @@ -65,6 +65,21 @@ export class Mutex implements PromiseLike { } } + /** + * Enqueue work with an awaitable result. + */ + produce(task: () => PromiseLike, cancel?: () => void): Promise { + return new Promise((resolve, reject) => { + this.run(async () => { + try { + resolve(await task()); + } catch (e) { + reject(e); + } + }, cancel); + }); + } + /** * Cancel remaining work and perform one last task with the Mutex held. */ diff --git a/packages/node/src/node/Node.ts b/packages/node/src/node/Node.ts index 392c05e20..f25349daa 100644 --- a/packages/node/src/node/Node.ts +++ b/packages/node/src/node/Node.ts @@ -82,6 +82,10 @@ export abstract class Node(); #isOnline = false; #isCommissioned = false; + #mutex: Mutex; constructor(endpoint: Endpoint) { super(endpoint); + this.#mutex = new Mutex(endpoint); + this.#online.on(() => { this.#isOnline = true; }); @@ -97,4 +100,17 @@ export class NodeLifecycle extends EndpointLifecycle { get decommissioned() { return this.#decommissioned; } + + /** + * Mutex for protecting node lifecycle transitions. + * + * Methods that implement complex async lifecycle transitions use this mutex to ensure conflicting operations cannot + * intermingle. + * + * Generally methods that hold this mutex have a protected "*WithMutex" variant. This allows for nesting of logic + * that requires the mutex without causing deadlock. + */ + get mutex() { + return this.#mutex; + } } diff --git a/packages/node/src/node/ServerNode.ts b/packages/node/src/node/ServerNode.ts index e4bda0fc0..4c5d4f8d7 100644 --- a/packages/node/src/node/ServerNode.ts +++ b/packages/node/src/node/ServerNode.ts @@ -114,20 +114,24 @@ export class ServerNode { await node.cancel(); } - await node.erase(); + await MockTime.resolve(node.erase()); // Confirm previous online state is resumed expect(node.lifecycle.isOnline).equals(mode === "online"); diff --git a/packages/protocol/src/mdns/MdnsService.ts b/packages/protocol/src/mdns/MdnsService.ts index a42696393..d011d3eb9 100644 --- a/packages/protocol/src/mdns/MdnsService.ts +++ b/packages/protocol/src/mdns/MdnsService.ts @@ -22,6 +22,7 @@ const logger = Logger.get("MDNS"); export class MdnsService { #broadcaster?: MdnsBroadcaster; #scanner?: MdnsScanner; + #env: Environment; readonly #construction: Construction; readonly #enableIpv4: boolean; readonly limitedToNetInterface?: string; @@ -31,6 +32,7 @@ export class MdnsService { } constructor(environment: Environment, options?: MdnsService.Options) { + this.#env = environment; environment.set(MdnsService, this); environment.runtime.add(this); @@ -78,6 +80,8 @@ export class MdnsService { } async [Symbol.asyncDispose]() { + this.#env.delete(MdnsService, this); + await this.#construction.close(async () => { const broadcasterDisposal = MaybePromise.then(this.#broadcaster?.close(), undefined, e => logger.error("Error disposing of MDNS broadcaster", e),