@seedcord/core 0.11.0 → 0.12.0-next.2
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/README.md +3 -3
- package/dist/Bus-VJw-QpoY.mjs +986 -0
- package/dist/Bus-VJw-QpoY.mjs.map +1 -0
- package/dist/{CoordinatedShutdown-H5YjZn8A.d.mts → CoordinatedShutdown-fPKLWjc5.d.mts} +13 -4
- package/dist/{CoordinatedStartup-CGnJrqcl.mjs → CoordinatedStartup-Zw_Lk7Zk.mjs} +22 -20
- package/dist/CoordinatedStartup-Zw_Lk7Zk.mjs.map +1 -0
- package/dist/Generated-ByC53_A2.d.mts +26 -0
- package/dist/HmrModuleHandler-Btgu8r96.mjs +165 -0
- package/dist/HmrModuleHandler-Btgu8r96.mjs.map +1 -0
- package/dist/{PluginHost-DB4j8unu.d.mts → PluginHost-DFnkIldj.d.mts} +43 -15
- package/dist/PluginHost-DTtxWRiL.mjs +552 -0
- package/dist/PluginHost-DTtxWRiL.mjs.map +1 -0
- package/dist/{PluginLifecycle-AhQ46NIr.mjs → PluginLifecycle-hx5zUc-P.mjs} +4 -4
- package/dist/PluginLifecycle-hx5zUc-P.mjs.map +1 -0
- package/dist/{ServerHost-DYeh0KTk.d.mts → ServerHost-B5o3ScCF.d.mts} +7 -6
- package/dist/{ServerHost-CtmAhEXn.mjs → ServerHost-CDaEDR4C.mjs} +17 -6
- package/dist/{ServerHost-CtmAhEXn.mjs.map → ServerHost-CDaEDR4C.mjs.map} +1 -1
- package/dist/bindViteEnv-NFzvdqlT.mjs +32 -0
- package/dist/{bindViteEnv-DWq5rftJ.mjs.map → bindViteEnv-NFzvdqlT.mjs.map} +1 -1
- package/dist/env-internal.index.mjs +1 -1
- package/dist/hmr.index.d.mts +20 -20
- package/dist/hmr.index.mjs +1 -1
- package/dist/{index-Du71Xx69.d.mts → index-DGHng1IN.d.mts} +115 -201
- package/dist/index.d.mts +321 -74
- package/dist/index.mjs +323 -102
- package/dist/index.mjs.map +1 -1
- package/dist/internal.index.d.mts +223 -83
- package/dist/internal.index.mjs +573 -74
- package/dist/internal.index.mjs.map +1 -1
- package/dist/node-internal.index.d.mts +8 -4
- package/dist/node-internal.index.mjs +19 -6
- package/dist/node-internal.index.mjs.map +1 -1
- package/dist/node.index.d.mts +3 -3
- package/dist/node.index.mjs +22 -28
- package/dist/node.index.mjs.map +1 -1
- package/dist/{phases-FTM69J3a.d.mts → phases-C2QCSqRu.d.mts} +164 -55
- package/dist/plugin.index.d.mts +3 -2
- package/dist/plugin.index.mjs +1 -1
- package/dist/subscribers-CGuVjbrg.mjs +982 -0
- package/dist/subscribers-CGuVjbrg.mjs.map +1 -0
- package/dist/{bindViteEnv-DWq5rftJ.mjs → usingCtx-Cf2cZi96.mjs} +1 -31
- package/package.json +14 -11
- package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +0 -1
- package/dist/HmrModuleHandler-8ZC-UuZh.mjs +0 -162
- package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +0 -1
- package/dist/PluginHost-B8kxLOeb.mjs +0 -245
- package/dist/PluginHost-B8kxLOeb.mjs.map +0 -1
- package/dist/PluginLifecycle-AhQ46NIr.mjs.map +0 -1
- package/dist/phases-BauKJOTT.mjs +0 -31
- package/dist/phases-BauKJOTT.mjs.map +0 -1
- package/dist/publishDefault-D3dAyzZ9.mjs +0 -7
- package/dist/publishDefault-D3dAyzZ9.mjs.map +0 -1
- package/dist/subscribers-Ce3SWy23.mjs +0 -1913
- package/dist/subscribers-Ce3SWy23.mjs.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { P as Kv, a as Bus, i as CoreBase, n as StartupPhase, r as TaskShutdownPhase } from "./phases-C2QCSqRu.mjs";
|
|
2
2
|
import { Logger } from "@seedcord/logger";
|
|
3
3
|
import { REST } from "@discordjs/rest";
|
|
4
4
|
import { HostPluginKeys } from "@seedcord/types/internal";
|
|
5
5
|
import { Config, FrameworkChannel, HmrAware, HmrUpdateEvent, IRateLimiter, Tail, TypedExclude } from "@seedcord/types";
|
|
6
|
+
import { Promisable } from "type-fest";
|
|
6
7
|
//#region src/plugin/brands.d.ts
|
|
7
8
|
declare const TransportBrand: unique symbol;
|
|
8
9
|
declare const RuntimeBrand: unique symbol;
|
|
@@ -35,7 +36,7 @@ interface PluginLifecycleSpec {
|
|
|
35
36
|
};
|
|
36
37
|
/** Defaults to `ShutdownPhase.Disconnect` and 10000ms. */
|
|
37
38
|
dispose?: {
|
|
38
|
-
phase?:
|
|
39
|
+
phase?: TaskShutdownPhase;
|
|
39
40
|
timeout?: number;
|
|
40
41
|
};
|
|
41
42
|
}
|
|
@@ -49,7 +50,7 @@ interface ResolvedPluginLifecycleSpec {
|
|
|
49
50
|
readonly timeout: number;
|
|
50
51
|
};
|
|
51
52
|
readonly dispose: {
|
|
52
|
-
readonly phase:
|
|
53
|
+
readonly phase: TaskShutdownPhase;
|
|
53
54
|
readonly timeout: number;
|
|
54
55
|
};
|
|
55
56
|
}
|
|
@@ -124,7 +125,7 @@ type RuntimeAssert<Plug, BotRt extends Runtime> = BrandRuntime<Plug> extends 'an
|
|
|
124
125
|
//#endregion
|
|
125
126
|
//#region src/plugin/Plugin.d.ts
|
|
126
127
|
interface Initializeable {
|
|
127
|
-
init():
|
|
128
|
+
init(): Promisable<void>;
|
|
128
129
|
}
|
|
129
130
|
/** @internal */
|
|
130
131
|
declare const resolvedSpecSlot: unique symbol;
|
|
@@ -138,7 +139,7 @@ declare const FixedScope: unique symbol;
|
|
|
138
139
|
* Base class for a seedcord plugin. Extend it and implement `init()`. `this.core` is the running
|
|
139
140
|
* bot.
|
|
140
141
|
*
|
|
141
|
-
*
|
|
142
|
+
* Take the host as your constructor's first parameter and pass it to `super()`. Add your own parameters after
|
|
142
143
|
* it. `attach()` types them at the call site.
|
|
143
144
|
*
|
|
144
145
|
* A plugin that narrows `transport` or `runtime` in `Opts` passes the same values to `super()`.
|
|
@@ -167,6 +168,7 @@ declare const FixedScope: unique symbol;
|
|
|
167
168
|
* ```
|
|
168
169
|
*/
|
|
169
170
|
declare abstract class Plugin<Opts extends PluginOptions = {}, TCore extends CoreBase = CoreBase, Fixed extends ScopeAxis = never> implements Initializeable, HmrAware {
|
|
171
|
+
#private;
|
|
170
172
|
private readonly host;
|
|
171
173
|
/** @internal */
|
|
172
174
|
protected static readonly [FixedScope]: Partial<DeclaredScope>;
|
|
@@ -185,18 +187,31 @@ declare abstract class Plugin<Opts extends PluginOptions = {}, TCore extends Cor
|
|
|
185
187
|
constructor(host: CoreBase, ...[spec]: PluginSpecArgs<Opts, Fixed>);
|
|
186
188
|
/** The host, typed to the transport whose `Plugin` base this class extends. */
|
|
187
189
|
protected get core(): TCore;
|
|
190
|
+
/**
|
|
191
|
+
* This plugin's records in the bot's `config.kv`, stored under its attach key. Two attachments of
|
|
192
|
+
* one plugin class each get their own records. Renaming the attach key leaves the records under
|
|
193
|
+
* the old key behind.
|
|
194
|
+
*
|
|
195
|
+
* @throws A **SeedcordTypeError** when something built the plugin with `new` outside `attach()`.
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* // works in a field initializer, the constructor, and every method
|
|
199
|
+
* private readonly jobs = this.kv.collection<Job>('job');
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
protected get kv(): Kv;
|
|
188
203
|
/**
|
|
189
204
|
* Runs in `StartupPhase.Configuration` by default.
|
|
190
205
|
* Move it to a different phase with the {@link PluginLifecycleSpec}.
|
|
191
206
|
*/
|
|
192
|
-
abstract init():
|
|
207
|
+
abstract init(): Promisable<void>;
|
|
193
208
|
/**
|
|
194
209
|
* Runs in the Ready phase, after every attached plugin's `init()` has resolved. The other Ready
|
|
195
210
|
* tasks run alongside it, including the http server binding its port.
|
|
196
211
|
*
|
|
197
212
|
* Override it when your work needs a logged-in client or a resolved application id.
|
|
198
213
|
*/
|
|
199
|
-
ready?():
|
|
214
|
+
ready?(): Promisable<void>;
|
|
200
215
|
/**
|
|
201
216
|
* Runs during teardown, in `ShutdownPhase.Disconnect` by default. seedcord skips it when `init()`
|
|
202
217
|
* has thrown.
|
|
@@ -204,7 +219,16 @@ declare abstract class Plugin<Opts extends PluginOptions = {}, TCore extends Cor
|
|
|
204
219
|
* When `init()` outlasts its timeout, seedcord calls this once that `init()` resolves, outside any
|
|
205
220
|
* shutdown phase. The process must still be alive for that call to happen.
|
|
206
221
|
*/
|
|
207
|
-
dispose?():
|
|
222
|
+
dispose?(): Promisable<void>;
|
|
223
|
+
/**
|
|
224
|
+
* Returns data that `seedcord codegen` writes into the bot's `seedcord-gen.ts` as a type.
|
|
225
|
+
* Read it back with `Generated<typeof YourPlugin>`. Return plain data, or classes and objects
|
|
226
|
+
* that a project file or your plugin's package exports. The class needs a `static readonly id`.
|
|
227
|
+
*
|
|
228
|
+
* `seedcord codegen` calls this without running `init()`. Read only what the constructor stored
|
|
229
|
+
* and the bot's files.
|
|
230
|
+
*/
|
|
231
|
+
codegen?(): Promise<unknown>;
|
|
208
232
|
/**
|
|
209
233
|
* Rejects the options this plugin got.
|
|
210
234
|
*
|
|
@@ -213,7 +237,7 @@ declare abstract class Plugin<Opts extends PluginOptions = {}, TCore extends Cor
|
|
|
213
237
|
*/
|
|
214
238
|
protected rejectOptions(reason: string): never;
|
|
215
239
|
/** Override to reload plugin state on an HMR update. */
|
|
216
|
-
onHmr(_event: HmrUpdateEvent):
|
|
240
|
+
onHmr(_event: HmrUpdateEvent): Promisable<void>;
|
|
217
241
|
/**
|
|
218
242
|
* Registers critical file patterns that should trigger a full restart when changed in Dev HMR.
|
|
219
243
|
* @param patterns - Glob patterns. A relative one resolves against `root` from `seedcord.config.ts`.
|
|
@@ -254,9 +278,13 @@ declare abstract class PluginHost<BotT extends Transport, BotRt extends Runtime>
|
|
|
254
278
|
abstract readonly config: Config;
|
|
255
279
|
abstract readonly rest: REST;
|
|
256
280
|
abstract readonly applicationId: string;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
281
|
+
/** @see {@link Bus} */
|
|
282
|
+
readonly bus: Bus;
|
|
283
|
+
/** @see {@link Kv} */
|
|
284
|
+
readonly kv: Kv;
|
|
285
|
+
/** @see {@link IRateLimiter} */
|
|
286
|
+
readonly rateLimiter: IRateLimiter;
|
|
287
|
+
constructor(transport: BotT, runtime: BotRt, driver: Config['kv']);
|
|
260
288
|
/** @internal */
|
|
261
289
|
get [attachmentsSlot](): readonly Attachment[];
|
|
262
290
|
/** @internal */
|
|
@@ -274,7 +302,7 @@ declare abstract class PluginHost<BotT extends Transport, BotRt extends Runtime>
|
|
|
274
302
|
* `start()`. An edge bot starts on its first request.
|
|
275
303
|
*
|
|
276
304
|
* Attaching a plugin whose `transport` or `runtime` differs from this host fails to compile.
|
|
277
|
-
*
|
|
305
|
+
* Take `CoreBase` as your constructor's first parameter. `attach()` passes it, so you don't.
|
|
278
306
|
* A narrower one fails to compile here.
|
|
279
307
|
*
|
|
280
308
|
* @param key - Also the channel the plugin logs on, dots included.
|
|
@@ -293,5 +321,5 @@ declare abstract class PluginHost<BotT extends Transport, BotRt extends Runtime>
|
|
|
293
321
|
declare function attachmentsOf(host: Pick<PluginHost<Transport, Runtime>, typeof attachmentsSlot>): readonly Attachment[];
|
|
294
322
|
declare function sealAttachments(host: Pick<PluginHost<Transport, Runtime>, typeof sealSlot>): void;
|
|
295
323
|
//#endregion
|
|
296
|
-
export { FixedScope as a, PluginArgs as c,
|
|
297
|
-
//# sourceMappingURL=PluginHost-
|
|
324
|
+
export { FixedScope as a, PluginArgs as c, Runtime as d, Transport as f, sealAttachments as i, PluginCtor as l, PluginHost as n, Initializeable as o, PluginLifecycleSpec as p, attachmentsOf as r, Plugin as s, Attachment as t, PluginOptions as u };
|
|
325
|
+
//# sourceMappingURL=PluginHost-DFnkIldj.d.mts.map
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { b as StartupPhase, t as Bus, v as MemoryDriver, y as ShutdownPhase } from "./Bus-VJw-QpoY.mjs";
|
|
2
|
+
import { SeedcordErrorCode, isSeedcordError } from "@seedcord/errors";
|
|
3
|
+
import { SeedcordError, SeedcordRangeError, SeedcordTypeError } from "@seedcord/errors/internal";
|
|
4
|
+
import { FRAMEWORK_CHANNELS, Logger } from "@seedcord/logger";
|
|
5
|
+
import { HostPluginKeys } from "@seedcord/types/internal";
|
|
6
|
+
|
|
7
|
+
//#region src/hmr/devChannel.ts
|
|
8
|
+
let channel;
|
|
9
|
+
/** @internal */
|
|
10
|
+
function setDevChannel(next) {
|
|
11
|
+
channel = next;
|
|
12
|
+
}
|
|
13
|
+
/** @internal */
|
|
14
|
+
function getDevChannel() {
|
|
15
|
+
return channel;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/plugin/lifecycle.ts
|
|
20
|
+
const DEFAULT_INIT_TIMEOUT_MS = 15e3;
|
|
21
|
+
const DEFAULT_READY_TIMEOUT_MS = 15e3;
|
|
22
|
+
const DEFAULT_DISPOSE_TIMEOUT_MS = 1e4;
|
|
23
|
+
function timeoutOf(field, declared, fallback, pluginName) {
|
|
24
|
+
if (declared === void 0) return fallback;
|
|
25
|
+
if (!Number.isFinite(declared) || declared <= 0) throw new SeedcordRangeError(SeedcordErrorCode.LifecycleInvalidTimeout, [`${pluginName}'s lifecycle ${field}.timeout`, declared]);
|
|
26
|
+
return declared;
|
|
27
|
+
}
|
|
28
|
+
/** @internal */
|
|
29
|
+
function resolveLifecycleSpec(spec, pluginName) {
|
|
30
|
+
return {
|
|
31
|
+
init: {
|
|
32
|
+
phase: spec?.init?.phase ?? 1,
|
|
33
|
+
timeout: timeoutOf("init", spec?.init?.timeout, DEFAULT_INIT_TIMEOUT_MS, pluginName)
|
|
34
|
+
},
|
|
35
|
+
ready: { timeout: timeoutOf("ready", spec?.ready?.timeout, DEFAULT_READY_TIMEOUT_MS, pluginName) },
|
|
36
|
+
dispose: {
|
|
37
|
+
phase: spec?.dispose?.phase ?? 3,
|
|
38
|
+
timeout: timeoutOf("dispose", spec?.dispose?.timeout, DEFAULT_DISPOSE_TIMEOUT_MS, pluginName)
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/plugin/Plugin.ts
|
|
45
|
+
let attaching;
|
|
46
|
+
/** @internal */
|
|
47
|
+
const resolvedSpecSlot = Symbol("seedcord:plugin:spec");
|
|
48
|
+
/** @internal */
|
|
49
|
+
const loggerSlot = Symbol("seedcord:plugin:logger");
|
|
50
|
+
/** @internal */
|
|
51
|
+
const scopeSlot = Symbol("seedcord:plugin:scope");
|
|
52
|
+
/** @internal */
|
|
53
|
+
const FixedScope = Symbol("seedcord:plugin:fixed-scope");
|
|
54
|
+
/**
|
|
55
|
+
* Base class for a seedcord plugin. Extend it and implement `init()`. `this.core` is the running
|
|
56
|
+
* bot.
|
|
57
|
+
*
|
|
58
|
+
* Take the host as your constructor's first parameter and pass it to `super()`. Add your own parameters after
|
|
59
|
+
* it. `attach()` types them at the call site.
|
|
60
|
+
*
|
|
61
|
+
* A plugin that narrows `transport` or `runtime` in `Opts` passes the same values to `super()`.
|
|
62
|
+
* `attach()` throws when either one differs from the bot's.
|
|
63
|
+
*
|
|
64
|
+
* @typeParam Opts - Where this plugin may attach.
|
|
65
|
+
* @typeParam TCore - The `Core` each transport binds `this.core` to.
|
|
66
|
+
* @typeParam Fixed - The `transport` and `runtime` values a transport's own `Plugin` base sets. Leave it out.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* class Analytics extends Plugin<{ transport: 'gateway' }> {
|
|
71
|
+
* constructor(
|
|
72
|
+
* host: CoreBase,
|
|
73
|
+
* private readonly apiKey: string
|
|
74
|
+
* ) {
|
|
75
|
+
* super(host, { transport: 'gateway', init: { phase: StartupPhase.Login } });
|
|
76
|
+
* }
|
|
77
|
+
*
|
|
78
|
+
* public async init(): Promise<void> {
|
|
79
|
+
* await this.connect(this.apiKey);
|
|
80
|
+
* }
|
|
81
|
+
* }
|
|
82
|
+
*
|
|
83
|
+
* seedcord.attach('analytics', Analytics, apiKey);
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
var Plugin = class {
|
|
87
|
+
host;
|
|
88
|
+
/** @internal */
|
|
89
|
+
static [FixedScope] = {};
|
|
90
|
+
/** @internal */
|
|
91
|
+
[resolvedSpecSlot];
|
|
92
|
+
/** @internal */
|
|
93
|
+
[loggerSlot];
|
|
94
|
+
/** @internal */
|
|
95
|
+
[scopeSlot];
|
|
96
|
+
/** Logs under the plugin's class name, on the channel its attach key sets. */
|
|
97
|
+
logger;
|
|
98
|
+
#kv;
|
|
99
|
+
constructor(host, ...[spec]) {
|
|
100
|
+
this.host = host;
|
|
101
|
+
const attachment = attaching;
|
|
102
|
+
attaching = void 0;
|
|
103
|
+
this.#kv = attachment?.kv;
|
|
104
|
+
this.logger = new Logger(this.constructor.name, attachment && { channel: attachment.key });
|
|
105
|
+
this[loggerSlot] = this.logger;
|
|
106
|
+
this[resolvedSpecSlot] = resolveLifecycleSpec(spec, this.constructor.name);
|
|
107
|
+
const passed = spec;
|
|
108
|
+
const fixed = new.target[FixedScope];
|
|
109
|
+
this[scopeSlot] = {
|
|
110
|
+
transport: fixed.transport ?? passed?.transport ?? "any",
|
|
111
|
+
runtime: fixed.runtime ?? passed?.runtime ?? "any"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The host, typed to the transport whose `Plugin` base this class extends. */
|
|
115
|
+
get core() {
|
|
116
|
+
return this.host;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* This plugin's records in the bot's `config.kv`, stored under its attach key. Two attachments of
|
|
120
|
+
* one plugin class each get their own records. Renaming the attach key leaves the records under
|
|
121
|
+
* the old key behind.
|
|
122
|
+
*
|
|
123
|
+
* @throws A **SeedcordTypeError** when something built the plugin with `new` outside `attach()`.
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* // works in a field initializer, the constructor, and every method
|
|
127
|
+
* private readonly jobs = this.kv.collection<Job>('job');
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
get kv() {
|
|
131
|
+
if (!this.#kv) throw new SeedcordTypeError(SeedcordErrorCode.CorePluginKvOutsideAttach, [this.constructor.name]);
|
|
132
|
+
return this.#kv;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Rejects the options this plugin got.
|
|
136
|
+
*
|
|
137
|
+
* @param reason - Why the options are invalid.
|
|
138
|
+
* @throws A **SeedcordTypeError** every time, with the plugin class name and `reason` in its message.
|
|
139
|
+
*/
|
|
140
|
+
rejectOptions(reason) {
|
|
141
|
+
throw new SeedcordTypeError(SeedcordErrorCode.PluginOptionsRejected, [this.constructor.name, reason]);
|
|
142
|
+
}
|
|
143
|
+
/** Override to reload plugin state on an HMR update. */
|
|
144
|
+
onHmr(_event) {
|
|
145
|
+
return Promise.resolve();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Registers critical file patterns that should trigger a full restart when changed in Dev HMR.
|
|
149
|
+
* @param patterns - Glob patterns. A relative one resolves against `root` from `seedcord.config.ts`.
|
|
150
|
+
*/
|
|
151
|
+
registerCriticalFiles(patterns) {
|
|
152
|
+
getDevChannel()?.send("seedcord:register-critical-files", { patterns });
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
/** @internal */
|
|
156
|
+
function resolvedLifecycleSpecOf(plugin) {
|
|
157
|
+
return plugin[resolvedSpecSlot];
|
|
158
|
+
}
|
|
159
|
+
/** @internal */
|
|
160
|
+
function extendsThisCorePlugin(ctor) {
|
|
161
|
+
return ctor.prototype instanceof Plugin;
|
|
162
|
+
}
|
|
163
|
+
/** @internal */
|
|
164
|
+
function pluginLoggerOf(plugin) {
|
|
165
|
+
return plugin[loggerSlot];
|
|
166
|
+
}
|
|
167
|
+
/** @internal */
|
|
168
|
+
function declaredScopeOf(plugin) {
|
|
169
|
+
return plugin[scopeSlot];
|
|
170
|
+
}
|
|
171
|
+
/** @internal */
|
|
172
|
+
function constructAttached(key, kv, construct) {
|
|
173
|
+
attaching = {
|
|
174
|
+
key,
|
|
175
|
+
kv
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
return construct();
|
|
179
|
+
} finally {
|
|
180
|
+
attaching = void 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/kv/GuardedDriver.ts
|
|
186
|
+
function translated(operation, key, error) {
|
|
187
|
+
if (isSeedcordError(error)) return error;
|
|
188
|
+
return new SeedcordError(SeedcordErrorCode.CoreKvDriverFailed, [operation, key], { cause: error });
|
|
189
|
+
}
|
|
190
|
+
async function guard(operation, key, run) {
|
|
191
|
+
try {
|
|
192
|
+
return await run();
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw translated(operation, key, error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
var GuardedDriver = class {
|
|
198
|
+
driver;
|
|
199
|
+
constructor(driver) {
|
|
200
|
+
this.driver = driver;
|
|
201
|
+
}
|
|
202
|
+
async get(key) {
|
|
203
|
+
return guard("read", key, () => this.driver.get(key));
|
|
204
|
+
}
|
|
205
|
+
async set(key, value, ttlMs) {
|
|
206
|
+
await guard("write", key, () => this.driver.set(key, value, ttlMs));
|
|
207
|
+
}
|
|
208
|
+
async delete(key) {
|
|
209
|
+
await guard("delete", key, () => this.driver.delete(key));
|
|
210
|
+
}
|
|
211
|
+
async *entries(prefix) {
|
|
212
|
+
try {
|
|
213
|
+
for await (const entry of this.driver.entries(prefix)) yield entry;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw translated("list", prefix, error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/kv/KeyedQueue.ts
|
|
222
|
+
var KeyedQueue = class {
|
|
223
|
+
tails = /* @__PURE__ */ new Map();
|
|
224
|
+
run(key, task) {
|
|
225
|
+
const run = (this.tails.get(key) ?? Promise.resolve()).then(task);
|
|
226
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
227
|
+
this.tails.set(key, tail);
|
|
228
|
+
tail.then(() => {
|
|
229
|
+
if (this.tails.get(key) === tail) this.tails.delete(key);
|
|
230
|
+
});
|
|
231
|
+
return run;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/kv/KvCollection.ts
|
|
237
|
+
/**
|
|
238
|
+
* The records of one type, stored under one name in `config.kv`. Get one from
|
|
239
|
+
* `kv.collection<T>('name')`.
|
|
240
|
+
*
|
|
241
|
+
* Every record goes through JSON on its way in and out. A `Date` you store reads back from `get()`
|
|
242
|
+
* as an ISO string.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts
|
|
246
|
+
* interface Profile { bio: string; joinedAt: Date }
|
|
247
|
+
* const profiles = this.core.kv.collection<Profile>('profile');
|
|
248
|
+
*
|
|
249
|
+
* await profiles.set(user.id, { bio: 'hi', joinedAt: new Date() });
|
|
250
|
+
* const profile = await profiles.get(user.id); // { bio: string; joinedAt: string } | undefined
|
|
251
|
+
* await profiles.delete(user.id);
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
var KvCollection = class {
|
|
255
|
+
#driver;
|
|
256
|
+
#prefix;
|
|
257
|
+
#updates;
|
|
258
|
+
constructor(driver, prefix, updates) {
|
|
259
|
+
this.#driver = driver;
|
|
260
|
+
this.#prefix = prefix;
|
|
261
|
+
this.#updates = updates;
|
|
262
|
+
}
|
|
263
|
+
/** Reads the record stored under `id`. Resolves to `undefined` when nothing is there or the record expired. */
|
|
264
|
+
async get(id) {
|
|
265
|
+
const key = this.#prefix + id;
|
|
266
|
+
const raw = await this.#driver.get(key);
|
|
267
|
+
const value = this.#liveValue(raw);
|
|
268
|
+
if (raw !== void 0 && value === void 0) await this.#deleteExpired(key);
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Stores `value` under `id` and replaces whatever was there.
|
|
273
|
+
*
|
|
274
|
+
* @param ttlMs - How long the record lasts, in milliseconds. Leave it out to keep the record until you delete it.
|
|
275
|
+
*/
|
|
276
|
+
async set(id, value, ttlMs) {
|
|
277
|
+
const key = this.#prefix + id;
|
|
278
|
+
await this.#updates.run(key, async () => this.#write(key, value, ttlMs));
|
|
279
|
+
}
|
|
280
|
+
/** Removes the record under `id`. */
|
|
281
|
+
async delete(id) {
|
|
282
|
+
const key = this.#prefix + id;
|
|
283
|
+
await this.#updates.run(key, async () => this.#driver.delete(key));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Stores what `next` returns for the record under `id`, then resolves to that value. `next` gets
|
|
287
|
+
* the current record, or `undefined` when there is none.
|
|
288
|
+
*
|
|
289
|
+
* Use it over a `get()` then a `set()` when two calls can change one record at the same time.
|
|
290
|
+
* `set()`, `delete()`, and `update()` calls on one id run one after the other in this process.
|
|
291
|
+
* None of them loses another's change. Two processes sharing one backend can still both read
|
|
292
|
+
* before either writes.
|
|
293
|
+
*
|
|
294
|
+
* @param ttlMs - How many milliseconds the stored record lasts. Pass a function to work it out from the record. Leave it out to keep the record until you delete it.
|
|
295
|
+
* @example
|
|
296
|
+
* ```ts
|
|
297
|
+
* // counts a run of the command and resolves to the new count
|
|
298
|
+
* const runs = await counts.update(user.id, (count) => (count ?? 0) + 1);
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
async update(id, next, ttlMs) {
|
|
302
|
+
const key = this.#prefix + id;
|
|
303
|
+
return this.#updates.run(key, async () => {
|
|
304
|
+
const value = next(this.#liveValue(await this.#driver.get(key)));
|
|
305
|
+
await this.#write(key, value, typeof ttlMs === "function" ? ttlMs(value) : ttlMs);
|
|
306
|
+
return value;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Yields `[id, record]` for every record in this collection that hasn't expired. Use it to read
|
|
311
|
+
* back what the bot saved before a restart.
|
|
312
|
+
*
|
|
313
|
+
* @throws A **SeedcordError** when the backend can't list its keys, like `@keyv/memcache`.
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* // schedules every saved reminder again after a restart
|
|
317
|
+
* for await (const [id, reminder] of reminders.entries()) schedule(id, reminder);
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
async *entries() {
|
|
321
|
+
const expired = [];
|
|
322
|
+
try {
|
|
323
|
+
for await (const [key, raw] of this.#driver.entries(this.#prefix)) {
|
|
324
|
+
const value = this.#liveValue(raw);
|
|
325
|
+
if (value === void 0) expired.push(key);
|
|
326
|
+
else yield [key.slice(this.#prefix.length), value];
|
|
327
|
+
}
|
|
328
|
+
} finally {
|
|
329
|
+
await Promise.all(expired.map(async (key) => this.#deleteExpired(key)));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async #write(key, value, ttlMs) {
|
|
333
|
+
const envelope = ttlMs === void 0 ? { value } : {
|
|
334
|
+
value,
|
|
335
|
+
expiresAt: Date.now() + ttlMs
|
|
336
|
+
};
|
|
337
|
+
await this.#driver.set(key, JSON.stringify(envelope), ttlMs);
|
|
338
|
+
}
|
|
339
|
+
async #deleteExpired(key) {
|
|
340
|
+
await this.#updates.run(key, async () => {
|
|
341
|
+
const raw = await this.#driver.get(key);
|
|
342
|
+
if (raw !== void 0 && this.#liveValue(raw) === void 0) await this.#driver.delete(key);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
#liveValue(raw) {
|
|
346
|
+
if (raw === void 0) return void 0;
|
|
347
|
+
const envelope = JSON.parse(raw);
|
|
348
|
+
if (envelope.expiresAt !== void 0 && envelope.expiresAt <= Date.now()) return void 0;
|
|
349
|
+
return envelope.value;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/kv/Kv.ts
|
|
355
|
+
function keyPrefix(path) {
|
|
356
|
+
return path.map((segment) => `${encodeURIComponent(segment)}:`).join("");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The bot's key-value store, or one prefixed slice of it. `core.kv` holds your handlers' records. A
|
|
360
|
+
* plugin's `this.kv` holds the plugin's.
|
|
361
|
+
*/
|
|
362
|
+
var Kv = class {
|
|
363
|
+
/** What you passed as `config.kv`. Register its type in `KvTypes` to call the library's own methods on it. */
|
|
364
|
+
driver;
|
|
365
|
+
#guarded;
|
|
366
|
+
#path;
|
|
367
|
+
#updates = new KeyedQueue();
|
|
368
|
+
constructor(driver, ...path) {
|
|
369
|
+
this.driver = driver;
|
|
370
|
+
this.#guarded = new GuardedDriver(driver);
|
|
371
|
+
this.#path = path;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Records of one type under `name`. Pass the type argument. A record type holding a function,
|
|
375
|
+
* `Map`, `Set`, or `bigint` fails to compile here.
|
|
376
|
+
*/
|
|
377
|
+
collection(name) {
|
|
378
|
+
return new KvCollection(this.#guarded, keyPrefix([...this.#path, name]), this.#updates);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region src/rateLimiter/KvRateLimiter.ts
|
|
384
|
+
function limitedResult(live, now) {
|
|
385
|
+
const resetAt = live.reduce((earliest, exp) => Math.min(earliest, exp), Number.POSITIVE_INFINITY);
|
|
386
|
+
return {
|
|
387
|
+
limited: true,
|
|
388
|
+
resetAt,
|
|
389
|
+
remaining: 0,
|
|
390
|
+
retryAfterMs: resetAt - now
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function openResult(remaining) {
|
|
394
|
+
return {
|
|
395
|
+
limited: false,
|
|
396
|
+
resetAt: 0,
|
|
397
|
+
remaining,
|
|
398
|
+
retryAfterMs: 0
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function limitOf(window) {
|
|
402
|
+
return Math.max(1, window.limit ?? 1);
|
|
403
|
+
}
|
|
404
|
+
function liveUses(stored, now) {
|
|
405
|
+
return (stored ?? []).filter((exp) => exp > now);
|
|
406
|
+
}
|
|
407
|
+
var KvRateLimiter = class {
|
|
408
|
+
uses;
|
|
409
|
+
constructor(kv) {
|
|
410
|
+
this.uses = kv.collection("rate-limit");
|
|
411
|
+
}
|
|
412
|
+
async charge(key, window) {
|
|
413
|
+
const limit = limitOf(window);
|
|
414
|
+
let result;
|
|
415
|
+
await this.uses.update(key, (stored) => {
|
|
416
|
+
const now = Date.now();
|
|
417
|
+
const live = liveUses(stored, now);
|
|
418
|
+
const limited = live.length >= limit;
|
|
419
|
+
if (!limited) live.push(now + window.windowMs);
|
|
420
|
+
result = limited ? limitedResult(live, now) : openResult(limit - live.length);
|
|
421
|
+
return live;
|
|
422
|
+
}, (live) => live.reduce((latest, exp) => Math.max(latest, exp), 0) - Date.now());
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
async peek(key, window) {
|
|
426
|
+
const stored = await this.uses.get(key);
|
|
427
|
+
const now = Date.now();
|
|
428
|
+
const limit = limitOf(window);
|
|
429
|
+
const live = liveUses(stored, now);
|
|
430
|
+
return live.length >= limit ? limitedResult(live, now) : openResult(limit - live.length);
|
|
431
|
+
}
|
|
432
|
+
async reset(key) {
|
|
433
|
+
await this.uses.delete(key);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/plugin/splitPluginKey.ts
|
|
439
|
+
function splitPluginKey(key) {
|
|
440
|
+
const dot = key.indexOf(".");
|
|
441
|
+
return dot === -1 ? {
|
|
442
|
+
head: key,
|
|
443
|
+
leaf: void 0
|
|
444
|
+
} : {
|
|
445
|
+
head: key.slice(0, dot),
|
|
446
|
+
leaf: key.slice(dot + 1)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/plugin/PluginHost.ts
|
|
452
|
+
const RESERVED_KEYS = new Set(FRAMEWORK_CHANNELS);
|
|
453
|
+
const attachmentsSlot = Symbol("seedcord:host:attachments");
|
|
454
|
+
const sealSlot = Symbol("seedcord:host:seal");
|
|
455
|
+
/** Base class for a plugin host. */
|
|
456
|
+
var PluginHost = class {
|
|
457
|
+
/** @see {@link Bus} */
|
|
458
|
+
bus;
|
|
459
|
+
/** @see {@link Kv} */
|
|
460
|
+
kv;
|
|
461
|
+
/** @see {@link IRateLimiter} */
|
|
462
|
+
rateLimiter;
|
|
463
|
+
#sealed = false;
|
|
464
|
+
#attachments = [];
|
|
465
|
+
#groups = /* @__PURE__ */ new Map();
|
|
466
|
+
#scope;
|
|
467
|
+
constructor(transport, runtime, driver) {
|
|
468
|
+
this.#scope = {
|
|
469
|
+
transport,
|
|
470
|
+
runtime
|
|
471
|
+
};
|
|
472
|
+
this.bus = new Bus(this);
|
|
473
|
+
const kvDriver = driver ?? new MemoryDriver();
|
|
474
|
+
this.kv = new Kv(kvDriver, "app");
|
|
475
|
+
this.rateLimiter = new KvRateLimiter(new Kv(kvDriver, "seedcord"));
|
|
476
|
+
}
|
|
477
|
+
/** @internal */
|
|
478
|
+
get [attachmentsSlot]() {
|
|
479
|
+
return this.#attachments;
|
|
480
|
+
}
|
|
481
|
+
/** @internal */
|
|
482
|
+
[sealSlot]() {
|
|
483
|
+
this.#sealed = true;
|
|
484
|
+
}
|
|
485
|
+
/** @internal codegen reads this to emit the `Core` augmentation */
|
|
486
|
+
get [HostPluginKeys]() {
|
|
487
|
+
return this.#attachments.map((attachment) => attachment.key);
|
|
488
|
+
}
|
|
489
|
+
attach(key, Plugin, ...args) {
|
|
490
|
+
if (this.#sealed) throw new SeedcordError(SeedcordErrorCode.CorePluginAfterInit);
|
|
491
|
+
const { head, leaf } = splitPluginKey(key);
|
|
492
|
+
if (RESERVED_KEYS.has(head)) throw new SeedcordTypeError(SeedcordErrorCode.CorePluginReservedChannel, [head]);
|
|
493
|
+
if (head === "" || leaf === "") throw new SeedcordTypeError(SeedcordErrorCode.CorePluginKeyMalformed, [key, "has an empty part."]);
|
|
494
|
+
if (leaf?.includes(".")) throw new SeedcordTypeError(SeedcordErrorCode.CorePluginKeyMalformed, [key, "has more than one dot."]);
|
|
495
|
+
this.#assertFree(head, leaf, key);
|
|
496
|
+
if (!extendsThisCorePlugin(Plugin)) throw new SeedcordTypeError(SeedcordErrorCode.CorePluginFromOtherCore, [Plugin.name]);
|
|
497
|
+
const kv = new Kv(this.kv.driver, "plugins", key);
|
|
498
|
+
const instance = constructAttached(key, kv, () => new Plugin(this, ...args));
|
|
499
|
+
this.#assertScope(Plugin.name, declaredScopeOf(instance));
|
|
500
|
+
this.#attachments.push({
|
|
501
|
+
key,
|
|
502
|
+
instance,
|
|
503
|
+
spec: resolvedLifecycleSpecOf(instance)
|
|
504
|
+
});
|
|
505
|
+
if (leaf === void 0) return Object.assign(this, { [key]: instance });
|
|
506
|
+
this.#groupFor(head)[leaf] = instance;
|
|
507
|
+
return this;
|
|
508
|
+
}
|
|
509
|
+
#assertFree(head, leaf, key) {
|
|
510
|
+
if (leaf === void 0) {
|
|
511
|
+
if (this.#groups.has(key)) throw new SeedcordTypeError(SeedcordErrorCode.CorePluginKeyHoldsGroup, [key]);
|
|
512
|
+
if (key in this) throw new SeedcordTypeError(SeedcordErrorCode.CorePluginKeyExists, [key]);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const group = this.#groups.get(head);
|
|
516
|
+
if (!group) {
|
|
517
|
+
if (head in this) throw new SeedcordTypeError(SeedcordErrorCode.CorePluginGroupTaken, [head, key]);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (Object.hasOwn(group, leaf)) throw new SeedcordTypeError(SeedcordErrorCode.CorePluginKeyExists, [key]);
|
|
521
|
+
}
|
|
522
|
+
#assertScope(pluginName, declared) {
|
|
523
|
+
for (const axis of ["transport", "runtime"]) {
|
|
524
|
+
const host = this.#scope[axis];
|
|
525
|
+
if (declared[axis] === "any" || declared[axis] === host) continue;
|
|
526
|
+
throw new SeedcordTypeError(SeedcordErrorCode.CorePluginScopeMismatch, [
|
|
527
|
+
pluginName,
|
|
528
|
+
axis,
|
|
529
|
+
declared[axis],
|
|
530
|
+
host
|
|
531
|
+
]);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
#groupFor(head) {
|
|
535
|
+
const existing = this.#groups.get(head);
|
|
536
|
+
if (existing) return existing;
|
|
537
|
+
const group = Object.create(null);
|
|
538
|
+
this.#groups.set(head, group);
|
|
539
|
+
Object.assign(this, { [head]: group });
|
|
540
|
+
return group;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
function attachmentsOf(host) {
|
|
544
|
+
return host[attachmentsSlot];
|
|
545
|
+
}
|
|
546
|
+
function sealAttachments(host) {
|
|
547
|
+
host[sealSlot]();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
//#endregion
|
|
551
|
+
export { KeyedQueue as a, pluginLoggerOf as c, splitPluginKey as i, getDevChannel as l, attachmentsOf as n, FixedScope as o, sealAttachments as r, Plugin as s, PluginHost as t, setDevChannel as u };
|
|
552
|
+
//# sourceMappingURL=PluginHost-DTtxWRiL.mjs.map
|