- Keep channels listed while Macs are offline, and share projects.
-
-
+ {offered
+ ? (
+ <>
+
+ {offered.host}{" "}
+ serves your team's directory. This Mac isn't in it, so its channels aren't listed
+ while it's offline.
+
+ {error &&
{error}
}
+
+
+
+
+ >
+ )
+ : (
+ <>
+
+ Keep channels listed while Macs are offline, and share projects.
+
+
+ >
+ )}
);
}
@@ -291,7 +350,7 @@ export function OfflineHost({ name, host: machine }: { name: string; host: strin
{name} runs on that Mac. Its chat opens here when the Mac is back.
diff --git a/apps/host/src/diagnostics.ts b/apps/host/src/diagnostics.ts
index 1cab67ff..ac997826 100644
--- a/apps/host/src/diagnostics.ts
+++ b/apps/host/src/diagnostics.ts
@@ -63,6 +63,7 @@ export async function diagnostics(sync?: boolean): Promise {
serving: served.isServing(),
shares: served.shares(),
hosts: hosts.map(({ address: _, ...host }) => host),
+ offered: directory.offered(),
},
shell: { ...environment },
web: web.address,
diff --git a/apps/host/src/directory.ts b/apps/host/src/directory.ts
index 100e6f2f..425d50c3 100644
--- a/apps/host/src/directory.ts
+++ b/apps/host/src/directory.ts
@@ -119,6 +119,21 @@ export function isChosen(): boolean {
return existsSync(config) || !!process.env.ACE_DIRECTORY;
}
+/** The directory each peer's host names, so a host without one can offer to join it. */
+const offers = new Map();
+
+export function offer(peer: string, value: string | undefined): void {
+ if (value) offers.set(peer, value);
+ else offers.delete(peer);
+}
+
+/** A directory a peer's host names while this host has none, as after leaving one. */
+export function offered(): { url: string; host: string } | undefined {
+ if (url()) return;
+ const [host, value] = offers.entries().next().value ?? [];
+ return host && value ? { url: value, host } : undefined;
+}
+
/**
* When each URL a peer named last failed without a secret, so discovery doesn't ask every 15
* seconds; its host may only be offline, so it's asked again later.
diff --git a/apps/host/src/peers.ts b/apps/host/src/peers.ts
index 5a80fe4d..3b7096ef 100644
--- a/apps/host/src/peers.ts
+++ b/apps/host/src/peers.ts
@@ -41,6 +41,7 @@ async function discover(signal: AbortSignal) {
if (online.get(name)?.address === host.machine.address) continue;
host.client.close();
hosts.delete(name);
+ directory.offer(name, undefined);
log("info", "peer.gone", { peer: name });
emit();
}
@@ -65,16 +66,17 @@ async function discover(signal: AbortSignal) {
});
hosts.set(name, host);
}
- // A teammate can start a directory after this host first greeted them, and a peer that answered
- // before `gh` resolved has a login to give.
- const chosen = directory.isChosen();
+ // A teammate can start a directory after this host first greeted them, a host that left one
+ // offers a teammate's instead, and a peer that answered before `gh` resolved has a login to give.
+ const listening = !directory.isChosen() || !directory.url();
for (const [name, { client, machine }] of hosts) {
- if (client.status === "open" && (!chosen || unnamed.has(name))) greet(client, machine);
+ if (client.status === "open" && (listening || unnamed.has(name))) greet(client, machine);
}
}
function greet(client: GatewayClient, machine: Machine) {
client.request({ op: "hello" }).then((hello) => {
+ directory.offer(machine.name, hello.directory);
if (hello.directory) void directory.adopt(hello.directory);
// A freshly started peer answers before `gh` has told it its owner's login, which may have changed.
if (!hello.github) return void unnamed.add(machine.name);
diff --git a/apps/host/src/protocol.ts b/apps/host/src/protocol.ts
index cf6ad01b..5d72279b 100644
--- a/apps/host/src/protocol.ts
+++ b/apps/host/src/protocol.ts
@@ -70,6 +70,8 @@ export type Diagnostics = {
shares: Share[];
/** Hosts in the directory's last answer, and when each last published to it; older hosts omit it. */
hosts?: DirectoryHost[];
+ /** A directory a teammate's host names while this host has none, such as after leaving it. */
+ offered?: { url: string; host: string };
};
/** Whether the helper loaded the login shell's environment; `pending` while it retries. */
shell: { state: "adopted" | "pending" | "failed"; error?: string };