@nectar-js/nectar 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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/cli-Ce-ZUj6M.js +2225 -0
- package/dist/cli-Ce-ZUj6M.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -0
- package/dist/index-DGMxBsub.d.ts +757 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins-CGvM19v9.js +663 -0
- package/dist/plugins-CGvM19v9.js.map +1 -0
- package/dist/registration-CaE0QBT6.js +545 -0
- package/dist/registration-CaE0QBT6.js.map +1 -0
- package/dist/runtime-CZJeZvSL.js +891 -0
- package/dist/runtime-CZJeZvSL.js.map +1 -0
- package/dist/start.d.ts +8 -0
- package/dist/start.js +15 -0
- package/dist/start.js.map +1 -0
- package/dist/testing.d.ts +111 -0
- package/dist/testing.js +320 -0
- package/dist/testing.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import { b as loadModule, h as paramValidatorsOf, m as findInvalidParam, t as PluginError, u as registerComponentRoutes, w as decodeCustomId } from "./plugins-CGvM19v9.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { Client, DiscordjsError, DiscordjsErrorCodes, Events } from "discord.js";
|
|
5
|
+
import { ApplicationCommandType, MessageFlags } from "discord-api-types/v10";
|
|
6
|
+
//#region src/components/matcher.ts
|
|
7
|
+
/**
|
|
8
|
+
* Resolves incoming custom IDs to routes.
|
|
9
|
+
*
|
|
10
|
+
* Custom IDs carry the route's short ID, so matching is a direct lookup rather than a pattern
|
|
11
|
+
* scan. The interaction kind is part of the key, so a button's ID sent back as a modal
|
|
12
|
+
* submission finds no route.
|
|
13
|
+
*/
|
|
14
|
+
function createMatcher(routes) {
|
|
15
|
+
const byId = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const route of routes) byId.set(`${route.kind}:${route.shortId}`, route);
|
|
17
|
+
return { match(kind, customId) {
|
|
18
|
+
const decoded = decodeCustomId(customId);
|
|
19
|
+
if (!decoded.ok) return decoded;
|
|
20
|
+
const route = byId.get(`${kind}:${decoded.shortId}`);
|
|
21
|
+
if (route === void 0) return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: "unknown-route"
|
|
24
|
+
};
|
|
25
|
+
const fixed = route.catchAll === null ? route.params.length : route.params.length - 1;
|
|
26
|
+
if (route.catchAll === null ? decoded.values.length !== fixed : decoded.values.length < fixed) return {
|
|
27
|
+
ok: false,
|
|
28
|
+
reason: "param-count"
|
|
29
|
+
};
|
|
30
|
+
const params = {};
|
|
31
|
+
for (let i = 0; i < fixed; i++) params[route.params[i]] = decoded.values[i];
|
|
32
|
+
if (route.catchAll !== null) params[route.catchAll] = decoded.values.slice(fixed);
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
route,
|
|
36
|
+
params
|
|
37
|
+
};
|
|
38
|
+
} };
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/manifest/load.ts
|
|
42
|
+
var ManifestVersionError = class extends Error {
|
|
43
|
+
file;
|
|
44
|
+
found;
|
|
45
|
+
constructor(file, found) {
|
|
46
|
+
super(`${file} is manifest version ${String(found)}, this build of @nectar-js/nectar reads version 1. Run \`nectar build\` again.`);
|
|
47
|
+
this.file = file;
|
|
48
|
+
this.found = found;
|
|
49
|
+
this.name = "ManifestVersionError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/** Reads a manifest from disk and checks its version. Does not validate the rest of the shape. */
|
|
53
|
+
function loadManifest(file) {
|
|
54
|
+
const absolute = path.resolve(file);
|
|
55
|
+
const parsed = JSON.parse(readFileSync(absolute, "utf8"));
|
|
56
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError(`${absolute} is not a manifest object.`);
|
|
57
|
+
const manifest = parsed;
|
|
58
|
+
if (manifest.version !== 1) throw new ManifestVersionError(absolute, manifest.version);
|
|
59
|
+
if (typeof manifest.appDir !== "string") throw new TypeError(`${absolute} has no appDir.`);
|
|
60
|
+
return {
|
|
61
|
+
manifest,
|
|
62
|
+
appDir: path.resolve(path.dirname(absolute), manifest.appDir)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/runtime/signals.ts
|
|
67
|
+
/**
|
|
68
|
+
* Fan-out for framework signals. Listeners run synchronously in subscription order; one that
|
|
69
|
+
* throws is reported through the logger and does not affect the others or the interaction.
|
|
70
|
+
*/
|
|
71
|
+
function createSignals(logger) {
|
|
72
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
73
|
+
return {
|
|
74
|
+
on(listener) {
|
|
75
|
+
listeners.add(listener);
|
|
76
|
+
return () => {
|
|
77
|
+
listeners.delete(listener);
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
emit(data) {
|
|
81
|
+
if (listeners.size === 0) return;
|
|
82
|
+
const signal = {
|
|
83
|
+
...data,
|
|
84
|
+
at: Date.now()
|
|
85
|
+
};
|
|
86
|
+
for (const listener of listeners) try {
|
|
87
|
+
listener(signal);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error(`A signal listener threw on ${data.type}.`, { error });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Reads the fields spec 31 asks for off a discord.js interaction, tolerating stubs. */
|
|
95
|
+
function interactionMeta(interaction) {
|
|
96
|
+
const i = interaction;
|
|
97
|
+
const guard = (name) => typeof i[name] === "function" && i[name]?.() === true;
|
|
98
|
+
const where = {
|
|
99
|
+
guildId: i.guildId ?? null,
|
|
100
|
+
channelId: i.channelId ?? null,
|
|
101
|
+
userId: i.user?.id ?? null
|
|
102
|
+
};
|
|
103
|
+
if (guard("isChatInputCommand") || guard("isAutocomplete")) return {
|
|
104
|
+
type: guard("isAutocomplete") ? "autocomplete" : "chatInput",
|
|
105
|
+
command: [
|
|
106
|
+
i.commandName,
|
|
107
|
+
i.options?.getSubcommandGroup(false) ?? null,
|
|
108
|
+
i.options?.getSubcommand(false) ?? null
|
|
109
|
+
].filter((p) => typeof p === "string").join(" "),
|
|
110
|
+
...where
|
|
111
|
+
};
|
|
112
|
+
if (guard("isContextMenuCommand")) return {
|
|
113
|
+
type: i.commandType === ApplicationCommandType.Message ? "messageContextMenu" : "userContextMenu",
|
|
114
|
+
command: i.commandName ?? "",
|
|
115
|
+
...where
|
|
116
|
+
};
|
|
117
|
+
const kind = guard("isButton") ? "button" : guard("isAnySelectMenu") ? "select" : guard("isModalSubmit") ? "modal" : null;
|
|
118
|
+
if (kind !== null) return {
|
|
119
|
+
type: kind,
|
|
120
|
+
customId: redactCustomId(i.customId ?? ""),
|
|
121
|
+
...where
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
type: "unknown",
|
|
125
|
+
...where
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Keeps the route part of a Nectar custom ID and hides the parameter values, which may carry
|
|
130
|
+
* anything the application put there. Other custom IDs are not ours and pass through.
|
|
131
|
+
*/
|
|
132
|
+
function redactCustomId(customId) {
|
|
133
|
+
if (!customId.startsWith("n:")) return customId;
|
|
134
|
+
const params = customId.indexOf(":", 2);
|
|
135
|
+
return params === -1 ? customId : `${customId.slice(0, params)}:*`;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/runtime/errors.ts
|
|
139
|
+
/** The reply the default boundary sends when an interaction is still unanswered. */
|
|
140
|
+
const GENERIC_ERROR_REPLY = "Something went wrong while handling that.";
|
|
141
|
+
/**
|
|
142
|
+
* Passes an error through the route's boundaries, nearest first, then the default boundary.
|
|
143
|
+
* A boundary handles the error by returning normally. Returning `"unhandled"` or throwing
|
|
144
|
+
* hands it (or the newly thrown error) to the next one. Nothing is ever swallowed: the default
|
|
145
|
+
* boundary always logs.
|
|
146
|
+
*
|
|
147
|
+
* `middleware` is the chain that ran before the handler; development output lists it.
|
|
148
|
+
* Resolves to the boundary file that handled the error, or `null` for the default boundary.
|
|
149
|
+
*/
|
|
150
|
+
async function handleError(error, ctx, boundaries, modules, logger, middleware = []) {
|
|
151
|
+
let current = error;
|
|
152
|
+
for (const file of boundaries) try {
|
|
153
|
+
if (await (await modules.loadDefault(file, "An error boundary"))(current, ctx) !== "unhandled") return file;
|
|
154
|
+
} catch (thrown) {
|
|
155
|
+
current = thrown;
|
|
156
|
+
}
|
|
157
|
+
await defaultBoundary(current, ctx, logger, middleware);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/** The structured metadata every framework log line about a route carries. */
|
|
161
|
+
function logFields(ctx, meta) {
|
|
162
|
+
const fields = { route: ctx.route.id };
|
|
163
|
+
if (!("interaction" in ctx)) {
|
|
164
|
+
fields.event = ctx.route.path.split("/")[0];
|
|
165
|
+
return fields;
|
|
166
|
+
}
|
|
167
|
+
const i = meta ?? interactionMeta(ctx.interaction);
|
|
168
|
+
fields.trace = ctx.trace.id;
|
|
169
|
+
fields.interaction = i.type;
|
|
170
|
+
if (i.command !== void 0) fields.command = i.command;
|
|
171
|
+
if (i.customId !== void 0) fields.customId = i.customId;
|
|
172
|
+
fields.guild = i.guildId;
|
|
173
|
+
fields.channel = i.channelId;
|
|
174
|
+
fields.user = i.userId;
|
|
175
|
+
return fields;
|
|
176
|
+
}
|
|
177
|
+
async function defaultBoundary(error, ctx, logger, middleware) {
|
|
178
|
+
logger.error(ctx.env === "development" ? developmentReport(ctx, middleware) : `Unhandled error in ${ctx.route.id} (${ctx.route.file})`, {
|
|
179
|
+
...logFields(ctx),
|
|
180
|
+
error
|
|
181
|
+
});
|
|
182
|
+
if (!("interaction" in ctx)) return;
|
|
183
|
+
const interaction = ctx.interaction;
|
|
184
|
+
if (typeof interaction.isRepliable !== "function" || !interaction.isRepliable()) return;
|
|
185
|
+
if (interaction.replied || interaction.deferred) return;
|
|
186
|
+
try {
|
|
187
|
+
await interaction.reply({
|
|
188
|
+
content: GENERIC_ERROR_REPLY,
|
|
189
|
+
flags: MessageFlags.Ephemeral
|
|
190
|
+
});
|
|
191
|
+
} catch (replyError) {
|
|
192
|
+
logger.error(`Could not send the error reply for ${ctx.route.id}`, {
|
|
193
|
+
...logFields(ctx),
|
|
194
|
+
error: replyError
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Where the failure sits in the app, so the developer can go straight to the boundary. */
|
|
199
|
+
function developmentReport(ctx, middleware) {
|
|
200
|
+
const rows = [["file", ctx.route.file]];
|
|
201
|
+
if ("interaction" in ctx) {
|
|
202
|
+
rows.push(["interaction", describeInteraction(ctx.interaction)]);
|
|
203
|
+
rows.push(["elapsed", `${ctx.trace.elapsed()}ms since Discord created it`]);
|
|
204
|
+
rows.push(["middleware", middleware.length === 0 ? "none" : middleware.join(`\n${" ".repeat(15)}`)]);
|
|
205
|
+
}
|
|
206
|
+
const width = Math.max(...rows.map(([key]) => key.length));
|
|
207
|
+
return [`Unhandled error in ${ctx.route.id}`, ...rows.map(([key, value]) => ` ${key.padEnd(width)} ${value}`)].join("\n");
|
|
208
|
+
}
|
|
209
|
+
function describeInteraction(interaction) {
|
|
210
|
+
const meta = interactionMeta(interaction);
|
|
211
|
+
let what;
|
|
212
|
+
switch (meta.type) {
|
|
213
|
+
case "chatInput":
|
|
214
|
+
what = `/${meta.command}`;
|
|
215
|
+
break;
|
|
216
|
+
case "autocomplete":
|
|
217
|
+
what = `autocomplete for /${meta.command}`;
|
|
218
|
+
break;
|
|
219
|
+
case "userContextMenu":
|
|
220
|
+
case "messageContextMenu":
|
|
221
|
+
what = `context menu "${meta.command}"`;
|
|
222
|
+
break;
|
|
223
|
+
case "unknown":
|
|
224
|
+
what = "unknown interaction";
|
|
225
|
+
break;
|
|
226
|
+
default: what = `${meta.type} "${meta.customId}"`;
|
|
227
|
+
}
|
|
228
|
+
const where = [
|
|
229
|
+
meta.guildId === null ? "direct message" : `guild ${meta.guildId}`,
|
|
230
|
+
meta.channelId === null ? null : `channel ${meta.channelId}`,
|
|
231
|
+
meta.userId === null ? null : `user ${meta.userId}`
|
|
232
|
+
].filter((p) => p !== null);
|
|
233
|
+
return `${what} (${where.join(", ")})`;
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/runtime/middleware.ts
|
|
237
|
+
/**
|
|
238
|
+
* Runs middleware outer to inner, then the handler.
|
|
239
|
+
*
|
|
240
|
+
* `next()` continues unchanged; `next({ member })` puts `member` on every downstream context.
|
|
241
|
+
* Returning without calling `next` stops the chain. Throwing anywhere unwinds to the caller,
|
|
242
|
+
* which hands it to the error boundaries. Code after `await next()` runs after the handler.
|
|
243
|
+
*/
|
|
244
|
+
async function runChain(middleware, ctx, handler, hooks = {}) {
|
|
245
|
+
await step(0);
|
|
246
|
+
async function step(index, current = ctx) {
|
|
247
|
+
const layer = middleware[index];
|
|
248
|
+
if (layer === void 0) {
|
|
249
|
+
hooks.handler?.();
|
|
250
|
+
return handler(current);
|
|
251
|
+
}
|
|
252
|
+
hooks.middleware?.(index);
|
|
253
|
+
let called = false;
|
|
254
|
+
const next = (extension) => {
|
|
255
|
+
if (called) throw new Error("next() was called twice in the same middleware.");
|
|
256
|
+
called = true;
|
|
257
|
+
const downstream = extension === void 0 ? current : {
|
|
258
|
+
...current,
|
|
259
|
+
...extension
|
|
260
|
+
};
|
|
261
|
+
return step(index + 1, downstream);
|
|
262
|
+
};
|
|
263
|
+
return layer(current, next);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/runtime/modules.ts
|
|
268
|
+
var HandlerLoadError = class extends Error {
|
|
269
|
+
file;
|
|
270
|
+
detail;
|
|
271
|
+
constructor(file, detail) {
|
|
272
|
+
super(`${file}: ${detail}`);
|
|
273
|
+
this.file = file;
|
|
274
|
+
this.detail = detail;
|
|
275
|
+
this.name = "HandlerLoadError";
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Imports handler modules once and caches them. Files are absolute paths taken from the
|
|
280
|
+
* manifest, so nothing here touches the filesystem beyond `import()`.
|
|
281
|
+
*/
|
|
282
|
+
var ModuleRegistry = class {
|
|
283
|
+
cache = /* @__PURE__ */ new Map();
|
|
284
|
+
load(file) {
|
|
285
|
+
let pending = this.cache.get(file);
|
|
286
|
+
if (pending === void 0) {
|
|
287
|
+
pending = loadModule(file).catch((error) => {
|
|
288
|
+
this.cache.delete(file);
|
|
289
|
+
throw new HandlerLoadError(file, error instanceof Error ? error.message : String(error));
|
|
290
|
+
});
|
|
291
|
+
this.cache.set(file, pending);
|
|
292
|
+
}
|
|
293
|
+
return pending;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Forgets cached modules so the next load imports them again. With no argument, forgets
|
|
297
|
+
* everything. Only useful together with `enableModuleReloading`; otherwise `import()` hands
|
|
298
|
+
* back the same instance.
|
|
299
|
+
*/
|
|
300
|
+
invalidate(files) {
|
|
301
|
+
if (files === void 0) {
|
|
302
|
+
this.cache.clear();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
for (const file of files) this.cache.delete(file);
|
|
306
|
+
}
|
|
307
|
+
/** Imports every file up front so a bad module fails startup instead of the first interaction. */
|
|
308
|
+
async preload(files) {
|
|
309
|
+
await Promise.all([...new Set(files)].map((file) => this.load(file)));
|
|
310
|
+
}
|
|
311
|
+
/** The default export of a file, checked to be a function. */
|
|
312
|
+
async loadDefault(file, what) {
|
|
313
|
+
const value = (await this.load(file)).default;
|
|
314
|
+
if (typeof value !== "function") throw new HandlerLoadError(file, `${what} must be the default export and a function, got ${describe$1(value)}.`);
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
/** A named export of a file, checked to be a function. */
|
|
318
|
+
async loadNamed(file, name, what) {
|
|
319
|
+
const value = (await this.load(file))[name];
|
|
320
|
+
if (typeof value !== "function") throw new HandlerLoadError(file, `${what} must be exported as "${name}" and be a function, got ${describe$1(value)}.`);
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
function describe$1(value) {
|
|
325
|
+
return value === void 0 ? "no export" : typeof value;
|
|
326
|
+
}
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/runtime/state.ts
|
|
329
|
+
function absolute(state, file) {
|
|
330
|
+
return path.join(state.appDir, ...file.split("/"));
|
|
331
|
+
}
|
|
332
|
+
function routeInfo(state, route) {
|
|
333
|
+
return {
|
|
334
|
+
id: route.id,
|
|
335
|
+
category: route.category,
|
|
336
|
+
path: route.path,
|
|
337
|
+
file: absolute(state, route.file)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function chains(state, route) {
|
|
341
|
+
return {
|
|
342
|
+
middleware: route.middleware.map((f) => absolute(state, f)),
|
|
343
|
+
errors: route.errors.map((f) => absolute(state, f))
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/runtime/dispatch.ts
|
|
348
|
+
function createInteractionDispatcher(state) {
|
|
349
|
+
const tables = buildTables(state.manifest.routes, state.manifest.commands);
|
|
350
|
+
return async (interaction) => {
|
|
351
|
+
const receivedAt = Date.now();
|
|
352
|
+
const meta = interactionMeta(interaction);
|
|
353
|
+
state.signals.emit({
|
|
354
|
+
type: "interaction:start",
|
|
355
|
+
trace: interaction.id,
|
|
356
|
+
interaction: meta
|
|
357
|
+
});
|
|
358
|
+
if (interaction.isChatInputCommand()) {
|
|
359
|
+
const key = [interaction.options.getSubcommandGroup(false), interaction.options.getSubcommand(false)].filter((p) => p !== null).join("/");
|
|
360
|
+
const route = tables.commands.get(commandKey(ApplicationCommandType.ChatInput, interaction.commandName, key));
|
|
361
|
+
if (route === void 0) return unknown(state, interaction, meta, `chat input command /${meta.command}`);
|
|
362
|
+
return run(state, route, interaction, meta, {}, receivedAt);
|
|
363
|
+
}
|
|
364
|
+
if (interaction.isContextMenuCommand()) {
|
|
365
|
+
const route = tables.commands.get(commandKey(interaction.commandType, interaction.commandName, ""));
|
|
366
|
+
if (route === void 0) return unknown(state, interaction, meta, `context menu command "${meta.command}"`);
|
|
367
|
+
return run(state, route, interaction, meta, {}, receivedAt);
|
|
368
|
+
}
|
|
369
|
+
if (interaction.isAutocomplete()) {
|
|
370
|
+
const key = [interaction.options.getSubcommandGroup(false), interaction.options.getSubcommand(false)].filter((p) => p !== null).join("/");
|
|
371
|
+
const command = tables.commands.get(commandKey(ApplicationCommandType.ChatInput, interaction.commandName, key));
|
|
372
|
+
const route = command === void 0 ? void 0 : tables.autocomplete.get(command.id);
|
|
373
|
+
const option = interaction.options.getFocused(true).name;
|
|
374
|
+
if (route === void 0 || !route.options.includes(option)) return unknown(state, interaction, meta, `autocomplete for /${meta.command} "${option}"`);
|
|
375
|
+
return run(state, route, interaction, meta, {}, receivedAt, option);
|
|
376
|
+
}
|
|
377
|
+
const component = interaction.isButton() ? ["button", interaction.customId] : interaction.isAnySelectMenu() ? ["select", interaction.customId] : interaction.isModalSubmit() ? ["modal", interaction.customId] : null;
|
|
378
|
+
if (component === null) {
|
|
379
|
+
state.signals.emit({
|
|
380
|
+
type: "interaction:reject",
|
|
381
|
+
trace: interaction.id,
|
|
382
|
+
interaction: meta,
|
|
383
|
+
reason: "unknown-interaction"
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const [kind, customId] = component;
|
|
388
|
+
const match = tables.matcher.match(kind, customId);
|
|
389
|
+
if (!match.ok) {
|
|
390
|
+
if (match.reason === "not-nectar") return;
|
|
391
|
+
state.logger.warn(`Ignoring ${kind} with custom ID "${meta.customId}": ${match.reason}.`, {
|
|
392
|
+
trace: interaction.id,
|
|
393
|
+
interaction: kind,
|
|
394
|
+
customId: meta.customId
|
|
395
|
+
});
|
|
396
|
+
state.signals.emit({
|
|
397
|
+
type: "interaction:reject",
|
|
398
|
+
trace: interaction.id,
|
|
399
|
+
interaction: meta,
|
|
400
|
+
reason: match.reason
|
|
401
|
+
});
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
return run(state, match.route, interaction, meta, match.params, receivedAt);
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
async function run(state, route, interaction, meta, params, receivedAt, autocompleteOption) {
|
|
408
|
+
const ctx = {
|
|
409
|
+
interaction,
|
|
410
|
+
client: state.client,
|
|
411
|
+
route: routeInfo(state, route),
|
|
412
|
+
params,
|
|
413
|
+
env: state.env,
|
|
414
|
+
trace: {
|
|
415
|
+
id: interaction.id,
|
|
416
|
+
receivedAt,
|
|
417
|
+
elapsed: () => Date.now() - interaction.createdTimestamp
|
|
418
|
+
},
|
|
419
|
+
services: state.services
|
|
420
|
+
};
|
|
421
|
+
const files = chains(state, route);
|
|
422
|
+
const tag = {
|
|
423
|
+
trace: interaction.id,
|
|
424
|
+
interaction: meta,
|
|
425
|
+
route: ctx.route
|
|
426
|
+
};
|
|
427
|
+
state.signals.emit({
|
|
428
|
+
type: "route:match",
|
|
429
|
+
...tag
|
|
430
|
+
});
|
|
431
|
+
let handlerStart = 0;
|
|
432
|
+
try {
|
|
433
|
+
const middleware = await Promise.all(files.middleware.map((file) => state.modules.loadDefault(file, "Middleware")));
|
|
434
|
+
const handler = autocompleteOption === void 0 ? await state.modules.loadDefault(ctx.route.file, "The handler") : await state.modules.loadNamed(ctx.route.file, autocompleteOption, "The autocomplete handler");
|
|
435
|
+
if (route.kind === "button" || route.kind === "select" || route.kind === "modal") {
|
|
436
|
+
const invalid = await findInvalidParam(validators(ctx.route.file, handler, route), params);
|
|
437
|
+
if (invalid !== null) {
|
|
438
|
+
state.logger.warn(`Rejected ${route.kind} for ${ctx.route.id}: "${invalid}" failed validation.`, {
|
|
439
|
+
...logFields(ctx, meta),
|
|
440
|
+
param: invalid
|
|
441
|
+
});
|
|
442
|
+
state.signals.emit({
|
|
443
|
+
type: "interaction:reject",
|
|
444
|
+
trace: interaction.id,
|
|
445
|
+
interaction: meta,
|
|
446
|
+
reason: "invalid-param",
|
|
447
|
+
route: ctx.route,
|
|
448
|
+
param: invalid
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
await runChain(middleware, ctx, handler, {
|
|
454
|
+
middleware: (index) => state.signals.emit({
|
|
455
|
+
type: "middleware:enter",
|
|
456
|
+
...tag,
|
|
457
|
+
file: files.middleware[index] ?? ""
|
|
458
|
+
}),
|
|
459
|
+
handler: () => {
|
|
460
|
+
handlerStart = Date.now();
|
|
461
|
+
state.signals.emit({
|
|
462
|
+
type: "handler:enter",
|
|
463
|
+
...tag
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
const handled = handlerStart !== 0;
|
|
468
|
+
if (handled) state.signals.emit({
|
|
469
|
+
type: "handler:complete",
|
|
470
|
+
...tag,
|
|
471
|
+
duration: Date.now() - handlerStart
|
|
472
|
+
});
|
|
473
|
+
const duration = Date.now() - receivedAt;
|
|
474
|
+
state.signals.emit({
|
|
475
|
+
type: "interaction:complete",
|
|
476
|
+
...tag,
|
|
477
|
+
duration,
|
|
478
|
+
handled
|
|
479
|
+
});
|
|
480
|
+
state.logger.debug(handled ? `Handled ${ctx.route.id} in ${duration}ms.` : `Middleware stopped ${ctx.route.id}.`, logFields(ctx, meta));
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const boundary = await handleError(error, ctx, files.errors, state.modules, state.logger, files.middleware);
|
|
483
|
+
state.signals.emit({
|
|
484
|
+
type: "interaction:fail",
|
|
485
|
+
...tag,
|
|
486
|
+
error,
|
|
487
|
+
boundary
|
|
488
|
+
});
|
|
489
|
+
if (boundary !== null) state.logger.debug(`${ctx.route.id} failed, handled by ${boundary}.`, {
|
|
490
|
+
...logFields(ctx, meta),
|
|
491
|
+
boundary,
|
|
492
|
+
error
|
|
493
|
+
});
|
|
494
|
+
if (autocompleteOption !== void 0) await closeAutocomplete(interaction);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/** Discord shows a spinner until autocomplete answers, so a failed handler answers with nothing. */
|
|
498
|
+
async function closeAutocomplete(interaction) {
|
|
499
|
+
if (interaction.responded) return;
|
|
500
|
+
try {
|
|
501
|
+
await interaction.respond([]);
|
|
502
|
+
} catch {}
|
|
503
|
+
}
|
|
504
|
+
function unknown(state, interaction, meta, what) {
|
|
505
|
+
state.logger.warn(`No route for ${what}. Run \`nectar sync\` if commands changed.`, {
|
|
506
|
+
trace: interaction.id,
|
|
507
|
+
interaction: meta.type,
|
|
508
|
+
command: meta.command
|
|
509
|
+
});
|
|
510
|
+
state.signals.emit({
|
|
511
|
+
type: "interaction:reject",
|
|
512
|
+
trace: interaction.id,
|
|
513
|
+
interaction: meta,
|
|
514
|
+
reason: "no-route"
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* The route's parameter validators, checked once per handler instance. The compiler checked
|
|
519
|
+
* the shape at build time; this repeats it so a JavaScript project or a hot-reloaded file
|
|
520
|
+
* fails the same way instead of at the first click.
|
|
521
|
+
*/
|
|
522
|
+
function validators(file, handler, route) {
|
|
523
|
+
const cached = validatorCache.get(handler);
|
|
524
|
+
if (cached !== void 0) return cached;
|
|
525
|
+
let result;
|
|
526
|
+
try {
|
|
527
|
+
result = paramValidatorsOf(handler, route);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
throw new HandlerLoadError(file, error instanceof Error ? error.message : String(error));
|
|
530
|
+
}
|
|
531
|
+
validatorCache.set(handler, result);
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
const validatorCache = /* @__PURE__ */ new WeakMap();
|
|
535
|
+
function buildTables(routes, commands) {
|
|
536
|
+
const commandRoutes = /* @__PURE__ */ new Map();
|
|
537
|
+
const autocomplete = /* @__PURE__ */ new Map();
|
|
538
|
+
const components = [];
|
|
539
|
+
for (const route of routes) if (route.kind === "command") commandRoutes.set(route.id, route);
|
|
540
|
+
else if (route.kind === "autocomplete") autocomplete.set(route.id, route);
|
|
541
|
+
else if (route.kind !== "event") components.push(route);
|
|
542
|
+
const table = /* @__PURE__ */ new Map();
|
|
543
|
+
for (const command of commands) for (const [key, id] of Object.entries(command.handlers)) {
|
|
544
|
+
const route = commandRoutes.get(id);
|
|
545
|
+
if (route !== void 0) table.set(commandKey(command.type, command.name, key), route);
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
commands: table,
|
|
549
|
+
autocomplete,
|
|
550
|
+
matcher: createMatcher(components)
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function commandKey(type, name, handlerKey) {
|
|
554
|
+
return `${type}:${name}:${handlerKey}`;
|
|
555
|
+
}
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region src/runtime/events.ts
|
|
558
|
+
/**
|
|
559
|
+
* One discord.js listener per event. It fans out to the compiled handlers in manifest order,
|
|
560
|
+
* sequentially or concurrently as the event's mode says. A `once` handler runs on the first
|
|
561
|
+
* emission only; when every handler of an event is spent, the listener is removed.
|
|
562
|
+
*
|
|
563
|
+
* Handler errors go to the route's boundaries and never reach the client's `error` event.
|
|
564
|
+
* Returns the bindings so the runtime can remove them on shutdown.
|
|
565
|
+
*/
|
|
566
|
+
function bindEvents(state) {
|
|
567
|
+
const routes = /* @__PURE__ */ new Map();
|
|
568
|
+
for (const route of state.manifest.routes) if (route.kind === "event") routes.set(route.id, route);
|
|
569
|
+
const bindings = [];
|
|
570
|
+
for (const event of state.manifest.events) {
|
|
571
|
+
const handlers = event.handlers.map((id) => routes.get(id)).filter((r) => r !== void 0);
|
|
572
|
+
if (handlers.length === 0) continue;
|
|
573
|
+
const spent = /* @__PURE__ */ new Set();
|
|
574
|
+
const listener = (...args) => {
|
|
575
|
+
const live = handlers.filter((h) => !spent.has(h.id));
|
|
576
|
+
for (const h of live) if (h.once) spent.add(h.id);
|
|
577
|
+
if (spent.size === handlers.length) state.client.off(event.name, listener);
|
|
578
|
+
return fanOut(state, event, live, args);
|
|
579
|
+
};
|
|
580
|
+
state.client.on(event.name, listener);
|
|
581
|
+
bindings.push({
|
|
582
|
+
name: event.name,
|
|
583
|
+
listener
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return bindings;
|
|
587
|
+
}
|
|
588
|
+
async function fanOut(state, event, handlers, args) {
|
|
589
|
+
if (event.mode === "concurrent") {
|
|
590
|
+
await Promise.all(handlers.map((route) => invoke(state, event, route, args)));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
for (const route of handlers) await invoke(state, event, route, args);
|
|
594
|
+
}
|
|
595
|
+
async function invoke(state, event, route, args) {
|
|
596
|
+
const ctx = {
|
|
597
|
+
client: state.client,
|
|
598
|
+
route: routeInfo(state, route),
|
|
599
|
+
env: state.env,
|
|
600
|
+
services: state.services
|
|
601
|
+
};
|
|
602
|
+
try {
|
|
603
|
+
await (await state.modules.loadDefault(ctx.route.file, "The handler"))(...args, ctx);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
const boundary = await handleError(error, ctx, chains(state, route).errors, state.modules, state.logger);
|
|
606
|
+
state.signals.emit({
|
|
607
|
+
type: "event:fail",
|
|
608
|
+
event: event.name,
|
|
609
|
+
route: ctx.route,
|
|
610
|
+
error,
|
|
611
|
+
boundary
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/runtime/logger.ts
|
|
617
|
+
const ORDER = {
|
|
618
|
+
debug: 0,
|
|
619
|
+
info: 1,
|
|
620
|
+
warn: 2,
|
|
621
|
+
error: 3
|
|
622
|
+
};
|
|
623
|
+
function createLogger(options = {}) {
|
|
624
|
+
const threshold = ORDER[options.level ?? "info"];
|
|
625
|
+
const sink = options.sink ?? consoleSink;
|
|
626
|
+
const log = (level, message, fields = {}) => {
|
|
627
|
+
if (ORDER[level] < threshold) return;
|
|
628
|
+
sink({
|
|
629
|
+
level,
|
|
630
|
+
message,
|
|
631
|
+
at: Date.now(),
|
|
632
|
+
fields
|
|
633
|
+
});
|
|
634
|
+
};
|
|
635
|
+
return {
|
|
636
|
+
debug: (message, fields) => log("debug", message, fields),
|
|
637
|
+
info: (message, fields) => log("info", message, fields),
|
|
638
|
+
warn: (message, fields) => log("warn", message, fields),
|
|
639
|
+
error: (message, fields) => log("error", message, fields)
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
/** `[nectar] message key=value ...` on the matching console method, then the error if any. */
|
|
643
|
+
const consoleSink = ({ level, message, fields }) => {
|
|
644
|
+
const { error, ...rest } = fields;
|
|
645
|
+
const pairs = Object.entries(rest).filter(([, value]) => value !== void 0 && value !== null).map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
646
|
+
const line = [`[nectar] ${message}`, ...pairs].join(" ");
|
|
647
|
+
if (error === void 0) console[level](line);
|
|
648
|
+
else console[level](line, error);
|
|
649
|
+
};
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/runtime/runtime.ts
|
|
652
|
+
/** `client.login` failed: Discord refused the token, or could not be reached. */
|
|
653
|
+
var LoginError = class extends Error {
|
|
654
|
+
invalidToken;
|
|
655
|
+
constructor(cause) {
|
|
656
|
+
const invalidToken = cause instanceof DiscordjsError && cause.code === DiscordjsErrorCodes.TokenInvalid;
|
|
657
|
+
super(invalidToken ? "Discord rejected the bot token." : `Could not log in: ${describe(cause)}`, { cause });
|
|
658
|
+
this.name = "LoginError";
|
|
659
|
+
this.invalidToken = invalidToken;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
function createRuntime(options) {
|
|
663
|
+
const env = options.env ?? envFromProcess();
|
|
664
|
+
const logger = options.logger ?? createLogger(options.config.logger);
|
|
665
|
+
const signals = options.signals ?? createSignals(logger);
|
|
666
|
+
if (options.config.observe !== void 0) signals.on(options.config.observe);
|
|
667
|
+
const client = options.client ?? new Client({
|
|
668
|
+
...options.config.client,
|
|
669
|
+
intents: options.config.intents,
|
|
670
|
+
...options.config.partials === void 0 ? {} : { partials: options.config.partials }
|
|
671
|
+
});
|
|
672
|
+
const state = {
|
|
673
|
+
manifest: options.manifest,
|
|
674
|
+
appDir: path.resolve(options.appDir),
|
|
675
|
+
client,
|
|
676
|
+
modules: new ModuleRegistry(),
|
|
677
|
+
env,
|
|
678
|
+
logger,
|
|
679
|
+
signals,
|
|
680
|
+
services: {}
|
|
681
|
+
};
|
|
682
|
+
const plugins = options.config.plugins ?? [];
|
|
683
|
+
const app = {
|
|
684
|
+
client,
|
|
685
|
+
env,
|
|
686
|
+
logger,
|
|
687
|
+
signals,
|
|
688
|
+
get manifest() {
|
|
689
|
+
return state.manifest;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
registerComponentRoutes(componentRoutes(state.manifest));
|
|
693
|
+
let dispatch = createInteractionDispatcher(state);
|
|
694
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
695
|
+
let bindings = [];
|
|
696
|
+
let started = false;
|
|
697
|
+
let globalStarted = false;
|
|
698
|
+
let drainTimeout = 1e4;
|
|
699
|
+
let stopping = null;
|
|
700
|
+
let onSignal = null;
|
|
701
|
+
const onInteraction = (interaction) => {
|
|
702
|
+
const task = dispatch(interaction).finally(() => inFlight.delete(task));
|
|
703
|
+
inFlight.add(task);
|
|
704
|
+
};
|
|
705
|
+
const onDisconnect = () => void runtime.stop();
|
|
706
|
+
const gateway = {
|
|
707
|
+
[Events.ShardReady]: (shard) => signals.emit({
|
|
708
|
+
type: "gateway:connect",
|
|
709
|
+
shard,
|
|
710
|
+
resumed: false
|
|
711
|
+
}),
|
|
712
|
+
[Events.ShardResume]: (shard) => signals.emit({
|
|
713
|
+
type: "gateway:connect",
|
|
714
|
+
shard,
|
|
715
|
+
resumed: true
|
|
716
|
+
}),
|
|
717
|
+
[Events.ShardDisconnect]: (event, shard) => signals.emit({
|
|
718
|
+
type: "gateway:disconnect",
|
|
719
|
+
shard,
|
|
720
|
+
code: event.code
|
|
721
|
+
})
|
|
722
|
+
};
|
|
723
|
+
const runtime = {
|
|
724
|
+
client,
|
|
725
|
+
env,
|
|
726
|
+
modules: state.modules,
|
|
727
|
+
signals,
|
|
728
|
+
async start({ token, signals: osSignals = true, drainTimeout: timeout = 1e4 }) {
|
|
729
|
+
drainTimeout = timeout;
|
|
730
|
+
await startPlugins(plugins, app, state.services);
|
|
731
|
+
if (runsShardZero(client.options.shards)) {
|
|
732
|
+
await startGlobal(plugins, app);
|
|
733
|
+
globalStarted = true;
|
|
734
|
+
}
|
|
735
|
+
if (options.config.eager ?? env === "production") await state.modules.preload(manifestFiles(state.manifest, state.appDir));
|
|
736
|
+
bindings = bindEvents(state);
|
|
737
|
+
started = true;
|
|
738
|
+
client.on(Events.InteractionCreate, onInteraction);
|
|
739
|
+
client.on(Events.ShardReady, gateway[Events.ShardReady]);
|
|
740
|
+
client.on(Events.ShardResume, gateway[Events.ShardResume]);
|
|
741
|
+
client.on(Events.ShardDisconnect, gateway[Events.ShardDisconnect]);
|
|
742
|
+
if (osSignals) {
|
|
743
|
+
onSignal = () => {
|
|
744
|
+
if (stopping !== null) {
|
|
745
|
+
logger.warn("Second signal received, exiting now.");
|
|
746
|
+
process.exit(1);
|
|
747
|
+
}
|
|
748
|
+
this.stop();
|
|
749
|
+
};
|
|
750
|
+
process.once("SIGINT", onSignal);
|
|
751
|
+
process.once("SIGTERM", onSignal);
|
|
752
|
+
if (process.connected) process.once("disconnect", onDisconnect);
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
await client.login(token);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
await this.stop();
|
|
758
|
+
throw new LoginError(error);
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
stop() {
|
|
762
|
+
if (stopping !== null) return stopping;
|
|
763
|
+
stopping = (async () => {
|
|
764
|
+
signals.emit({ type: "shutdown" });
|
|
765
|
+
client.off(Events.InteractionCreate, onInteraction);
|
|
766
|
+
client.off(Events.ShardReady, gateway[Events.ShardReady]);
|
|
767
|
+
client.off(Events.ShardResume, gateway[Events.ShardResume]);
|
|
768
|
+
client.off(Events.ShardDisconnect, gateway[Events.ShardDisconnect]);
|
|
769
|
+
for (const { name, listener } of bindings) client.off(name, listener);
|
|
770
|
+
bindings = [];
|
|
771
|
+
if (onSignal !== null) {
|
|
772
|
+
process.off("SIGINT", onSignal);
|
|
773
|
+
process.off("SIGTERM", onSignal);
|
|
774
|
+
onSignal = null;
|
|
775
|
+
}
|
|
776
|
+
process.off("disconnect", onDisconnect);
|
|
777
|
+
await drain(inFlight, drainTimeout, logger);
|
|
778
|
+
if (globalStarted) await stopPlugins(plugins, app, logger, "stopGlobal");
|
|
779
|
+
await stopPlugins(plugins, app, logger, "stop");
|
|
780
|
+
if (process.send !== void 0) ignoreClosedChannel();
|
|
781
|
+
await client.destroy();
|
|
782
|
+
})();
|
|
783
|
+
return stopping;
|
|
784
|
+
},
|
|
785
|
+
update(manifest) {
|
|
786
|
+
state.manifest = manifest;
|
|
787
|
+
registerComponentRoutes(componentRoutes(manifest));
|
|
788
|
+
dispatch = createInteractionDispatcher(state);
|
|
789
|
+
if (started && stopping === null) {
|
|
790
|
+
for (const { name, listener } of bindings) client.off(name, listener);
|
|
791
|
+
bindings = bindEvents(state);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
return runtime;
|
|
796
|
+
}
|
|
797
|
+
/** Runs `start` hooks in config order. Two plugins offering the same service is a startup failure. */
|
|
798
|
+
async function startPlugins(plugins, app, services) {
|
|
799
|
+
const providers = /* @__PURE__ */ new Map();
|
|
800
|
+
for (const plugin of plugins) {
|
|
801
|
+
let provided;
|
|
802
|
+
try {
|
|
803
|
+
provided = await plugin.start?.(app);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
throw new PluginError(plugin.name, `start failed: ${describe(error)}`);
|
|
806
|
+
}
|
|
807
|
+
if (provided === void 0 || provided === null) continue;
|
|
808
|
+
if (typeof provided !== "object") throw new PluginError(plugin.name, `start must return an object of services or nothing.`);
|
|
809
|
+
for (const [name, service] of Object.entries(provided)) {
|
|
810
|
+
const owner = providers.get(name);
|
|
811
|
+
if (owner !== void 0) throw new PluginError(plugin.name, `provides service "${name}", which plugin "${owner}" already provides.`);
|
|
812
|
+
providers.set(name, plugin.name);
|
|
813
|
+
services[name] = service;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
let ignoringClosedChannel = false;
|
|
818
|
+
/**
|
|
819
|
+
* discord.js's shard client reports gateway events to the manager with `process.send`, and
|
|
820
|
+
* destroying the client produces some. If the manager has gone, each report fails with an
|
|
821
|
+
* `error` event on `process` that would crash it. Only that failure is dropped; the listener
|
|
822
|
+
* stays, since the failures arrive on a later tick.
|
|
823
|
+
*/
|
|
824
|
+
function ignoreClosedChannel() {
|
|
825
|
+
if (ignoringClosedChannel) return;
|
|
826
|
+
ignoringClosedChannel = true;
|
|
827
|
+
process.on("error", (error) => {
|
|
828
|
+
if (error.code !== "ERR_IPC_CHANNEL_CLOSED") throw error;
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
/** Runs `startGlobal` hooks in config order. */
|
|
832
|
+
async function startGlobal(plugins, app) {
|
|
833
|
+
for (const plugin of plugins) try {
|
|
834
|
+
await plugin.startGlobal?.(app);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
throw new PluginError(plugin.name, `startGlobal failed: ${describe(error)}`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/** Runs `stopGlobal` or `stop` hooks in reverse order. A failing hook is logged; shutdown continues. */
|
|
840
|
+
async function stopPlugins(plugins, app, logger, hook) {
|
|
841
|
+
for (const plugin of [...plugins].reverse()) try {
|
|
842
|
+
await plugin[hook]?.(app);
|
|
843
|
+
} catch (error) {
|
|
844
|
+
logger.error(`Plugin "${plugin.name}": ${hook} failed.`, {
|
|
845
|
+
plugin: plugin.name,
|
|
846
|
+
error
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Whether this process runs shard 0. Application-global hooks run there, so they run once
|
|
852
|
+
* however the shards are spread over processes. `auto` means this process runs them all.
|
|
853
|
+
*/
|
|
854
|
+
function runsShardZero(shards) {
|
|
855
|
+
if (shards === void 0 || shards === "auto") return true;
|
|
856
|
+
return typeof shards === "number" ? shards === 0 : shards.includes(0);
|
|
857
|
+
}
|
|
858
|
+
function describe(error) {
|
|
859
|
+
return error instanceof Error ? error.message : String(error);
|
|
860
|
+
}
|
|
861
|
+
async function drain(inFlight, timeout, logger) {
|
|
862
|
+
if (inFlight.size === 0) return;
|
|
863
|
+
let timer;
|
|
864
|
+
const expired = new Promise((resolve) => {
|
|
865
|
+
timer = setTimeout(() => resolve("timeout"), timeout);
|
|
866
|
+
});
|
|
867
|
+
const result = await Promise.race([Promise.allSettled([...inFlight]), expired]);
|
|
868
|
+
clearTimeout(timer);
|
|
869
|
+
if (result === "timeout") logger.warn(`${inFlight.size} interaction(s) still running after ${timeout}ms, shutting down anyway.`);
|
|
870
|
+
}
|
|
871
|
+
function componentRoutes(manifest) {
|
|
872
|
+
return manifest.routes.filter((r) => r.kind === "button" || r.kind === "select" || r.kind === "modal");
|
|
873
|
+
}
|
|
874
|
+
/** Absolute paths of every handler, middleware, and error boundary file a manifest refers to. */
|
|
875
|
+
function manifestFiles(manifest, appDir) {
|
|
876
|
+
const files = /* @__PURE__ */ new Set();
|
|
877
|
+
for (const route of manifest.routes) {
|
|
878
|
+
files.add(route.file);
|
|
879
|
+
for (const file of route.middleware) files.add(file);
|
|
880
|
+
for (const file of route.errors) files.add(file);
|
|
881
|
+
}
|
|
882
|
+
return new Set([...files].map((file) => path.join(appDir, ...file.split("/"))));
|
|
883
|
+
}
|
|
884
|
+
function envFromProcess() {
|
|
885
|
+
const value = process.env.NODE_ENV;
|
|
886
|
+
return value === "production" || value === "test" ? value : "development";
|
|
887
|
+
}
|
|
888
|
+
//#endregion
|
|
889
|
+
export { bindEvents as a, ModuleRegistry as c, createLogger as i, createSignals as l, createRuntime as n, createInteractionDispatcher as o, manifestFiles as r, HandlerLoadError as s, LoginError as t, loadManifest as u };
|
|
890
|
+
|
|
891
|
+
//# sourceMappingURL=runtime-CZJeZvSL.js.map
|