@beta Multi-instance ClientCluster example.
multi-instance-bot.js · 200 lines
@beta Multi-instance ClientCluster example.
View on GitHubnode examples/multi-instance-bot.js
javascript
/**
* @beta Multi-instance ClientCluster example.
*
* ClientCluster is BETA — API may change. Each runtime needs its OWN instance-issued token.
* Tokens are not portable across hosted / self-hosted instances.
*
* Usage (from repo root after pnpm install && pnpm run build):
*
* # Hosted only
* FLUXER_BOT_TOKEN=hosted_token node examples/multi-instance-bot.js
*
* # Self-hosted only (discovery)
* SELFHOST_API=https://api.my.instance SELFHOST_BOT_TOKEN=self_token node examples/multi-instance-bot.js
*
* # Both at startup (distinct tokens required)
* FLUXER_BOT_TOKEN=hosted_token SELFHOST_API=https://api.my.instance SELFHOST_BOT_TOKEN=self_token node examples/multi-instance-bot.js
*
* # Dynamic add after startup (no process restart):
* # send !add-self in a channel the hosted bot can see (requires SELFHOST_API + SELFHOST_BOT_TOKEN)
* # send !remove-self to disconnect the self-hosted runtime only
* # send !restart-self to rebuild selfhosted with the same env token (token re-supplied)
*
* Optional legacy: FLUXER_API_URL overrides the hosted client's API host via ClientOptions.rest.api
* (does not replace SELFHOST_API discovery).
*/
import {
ClientCluster,
ClientClusterEvents,
Events,
BETA_CLIENT_CLUSTER_WARNING,
} from '@fluxerjs/core';
console.warn(`[beta] ${BETA_CLIENT_CLUSTER_WARNING}`);
const hostedToken = process.env.FLUXER_BOT_TOKEN;
const selfApi = process.env.SELFHOST_API;
const selfToken = process.env.SELFHOST_BOT_TOKEN;
const legacyApiUrl = process.env.FLUXER_API_URL;
if (!hostedToken && !(selfApi && selfToken)) {
console.error(
'Set FLUXER_BOT_TOKEN and/or SELFHOST_API + SELFHOST_BOT_TOKEN (distinct tokens; see examples/README.md).',
);
process.exit(1);
}
if (selfApi && !selfToken) {
console.error('SELFHOST_API requires SELFHOST_BOT_TOKEN (do not reuse FLUXER_BOT_TOKEN).');
process.exit(1);
}
if (hostedToken && selfToken && hostedToken === selfToken) {
console.error('Hosted and self-hosted runtimes need different tokens issued by each instance.');
process.exit(1);
}
/** @type {import('@fluxerjs/core').ClientCluster | undefined} */
let cluster;
/** @param {import('@fluxerjs/core').ClientRuntime} runtime */
function wireHandlers(runtime) {
const { id, client } = runtime;
client.on(Events.Ready, () => {
console.log(
`[${id}] ready as ${client.user?.username ?? '?'} @ ${client.instance.endpoints.api}`,
);
});
client.on(Events.Error, (err) => {
console.error(`[${id}] error`, err);
});
client.on(Events.MessageCreate, async (message) => {
if (message.author?.bot) return;
if (message.content === '!ping') {
await message.reply(`Pong from \`${id}\` (${client.instance.endpoints.api})`);
}
if (message.content === '!instance') {
const ep = client.instance.endpoints;
await message.reply(
[
`runtime: \`${id}\``,
`status: ${runtime.status}`,
`lastError: ${runtime.lastError?.message ?? 'none'}`,
`api: ${ep.api}`,
`media: ${ep.media}`,
`invite: ${ep.invite}`,
`self_hosted: ${client.instance.discovery?.features?.self_hosted ?? 'unknown'}`,
'',
'ClientCluster is beta — each runtime needs its own instance token.',
].join('\n'),
);
}
if (message.content === '!add-self') {
if (!cluster) return;
if (cluster.has('selfhosted')) {
await message.reply('`selfhosted` is already connected.');
return;
}
if (!selfApi || !selfToken) {
await message.reply(
'Set SELFHOST_API and SELFHOST_BOT_TOKEN to add a self-hosted runtime.',
);
return;
}
try {
await cluster.add({
id: 'selfhosted',
token: selfToken,
discovery: selfApi,
});
await message.reply('Added `selfhosted` runtime (no process restart).');
} catch (err) {
await message.reply(
`Failed to add selfhosted: ${err instanceof Error ? err.message : err}`,
);
}
}
if (message.content === '!remove-self') {
if (!cluster) return;
const ok = await cluster.remove('selfhosted');
await message.reply(ok ? 'Removed `selfhosted`.' : '`selfhosted` was not connected.');
}
if (message.content === '!restart-self') {
if (!cluster) return;
if (!selfToken) {
await message.reply('Set SELFHOST_BOT_TOKEN to restart (token must be re-supplied).');
return;
}
if (!cluster.has('selfhosted')) {
await message.reply('`selfhosted` is not connected. Use !add-self first.');
return;
}
try {
await cluster.restart('selfhosted', { token: selfToken });
await message.reply('Restarted `selfhosted` (token re-supplied; no process restart).');
} catch (err) {
await message.reply(
`Failed to restart selfhosted: ${err instanceof Error ? err.message : err}`,
);
}
}
if (message.content === '!runtimes') {
if (!cluster) return;
const list = cluster.values().map((r) => {
const err = r.lastError ? ` err=${r.lastError.message}` : '';
return `- ${r.id} (${r.status})${err} ${r.client.instance.endpoints.api}`;
});
await message.reply(list.length ? list.join('\n') : 'No runtimes.');
}
});
}
cluster = new ClientCluster({
suppressBetaWarning: true, // we already logged BETA_CLIENT_CLUSTER_WARNING above
configure: wireHandlers,
});
cluster.on(ClientClusterEvents.RuntimeReady, (rt) => {
console.log(`[cluster] runtimeReady ${rt.id}`);
});
cluster.on(ClientClusterEvents.RuntimeRemoved, (rt) => {
console.log(`[cluster] runtimeRemoved ${rt.id}`);
});
cluster.on(ClientClusterEvents.RuntimeError, (rt, err) => {
console.error(`[cluster] runtimeError ${rt.id}`, err);
});
if (hostedToken) {
await cluster.add({
id: 'hosted',
token: hostedToken,
...(legacyApiUrl ? { clientOptions: { rest: { api: legacyApiUrl } } } : {}),
});
}
if (selfApi && selfToken) {
await cluster.add({
id: 'selfhosted',
token: selfToken,
discovery: selfApi,
});
}
if (cluster.size === 0) {
console.error('No runtimes started.');
process.exit(1);
}
console.log(
`Cluster running with ${cluster.size} runtime(s). Try !ping, !instance, !runtimes, !add-self, !remove-self, !restart-self.`,
);
const shutdown = async () => {
console.log('Shutting down cluster…');
await cluster.destroy();
process.exit(0);
};
process.on('SIGINT', () => void shutdown());
process.on('SIGTERM', () => void shutdown());