@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.
Files changed (54) hide show
  1. package/README.md +3 -3
  2. package/dist/Bus-VJw-QpoY.mjs +986 -0
  3. package/dist/Bus-VJw-QpoY.mjs.map +1 -0
  4. package/dist/{CoordinatedShutdown-H5YjZn8A.d.mts → CoordinatedShutdown-fPKLWjc5.d.mts} +13 -4
  5. package/dist/{CoordinatedStartup-CGnJrqcl.mjs → CoordinatedStartup-Zw_Lk7Zk.mjs} +22 -20
  6. package/dist/CoordinatedStartup-Zw_Lk7Zk.mjs.map +1 -0
  7. package/dist/Generated-ByC53_A2.d.mts +26 -0
  8. package/dist/HmrModuleHandler-Btgu8r96.mjs +165 -0
  9. package/dist/HmrModuleHandler-Btgu8r96.mjs.map +1 -0
  10. package/dist/{PluginHost-DB4j8unu.d.mts → PluginHost-DFnkIldj.d.mts} +43 -15
  11. package/dist/PluginHost-DTtxWRiL.mjs +552 -0
  12. package/dist/PluginHost-DTtxWRiL.mjs.map +1 -0
  13. package/dist/{PluginLifecycle-AhQ46NIr.mjs → PluginLifecycle-hx5zUc-P.mjs} +4 -4
  14. package/dist/PluginLifecycle-hx5zUc-P.mjs.map +1 -0
  15. package/dist/{ServerHost-DYeh0KTk.d.mts → ServerHost-B5o3ScCF.d.mts} +7 -6
  16. package/dist/{ServerHost-CtmAhEXn.mjs → ServerHost-CDaEDR4C.mjs} +17 -6
  17. package/dist/{ServerHost-CtmAhEXn.mjs.map → ServerHost-CDaEDR4C.mjs.map} +1 -1
  18. package/dist/bindViteEnv-NFzvdqlT.mjs +32 -0
  19. package/dist/{bindViteEnv-DWq5rftJ.mjs.map → bindViteEnv-NFzvdqlT.mjs.map} +1 -1
  20. package/dist/env-internal.index.mjs +1 -1
  21. package/dist/hmr.index.d.mts +20 -20
  22. package/dist/hmr.index.mjs +1 -1
  23. package/dist/{index-Du71Xx69.d.mts → index-DGHng1IN.d.mts} +115 -201
  24. package/dist/index.d.mts +321 -74
  25. package/dist/index.mjs +323 -102
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/internal.index.d.mts +223 -83
  28. package/dist/internal.index.mjs +573 -74
  29. package/dist/internal.index.mjs.map +1 -1
  30. package/dist/node-internal.index.d.mts +8 -4
  31. package/dist/node-internal.index.mjs +19 -6
  32. package/dist/node-internal.index.mjs.map +1 -1
  33. package/dist/node.index.d.mts +3 -3
  34. package/dist/node.index.mjs +22 -28
  35. package/dist/node.index.mjs.map +1 -1
  36. package/dist/{phases-FTM69J3a.d.mts → phases-C2QCSqRu.d.mts} +164 -55
  37. package/dist/plugin.index.d.mts +3 -2
  38. package/dist/plugin.index.mjs +1 -1
  39. package/dist/subscribers-CGuVjbrg.mjs +982 -0
  40. package/dist/subscribers-CGuVjbrg.mjs.map +1 -0
  41. package/dist/{bindViteEnv-DWq5rftJ.mjs → usingCtx-Cf2cZi96.mjs} +1 -31
  42. package/package.json +14 -11
  43. package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +0 -1
  44. package/dist/HmrModuleHandler-8ZC-UuZh.mjs +0 -162
  45. package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +0 -1
  46. package/dist/PluginHost-B8kxLOeb.mjs +0 -245
  47. package/dist/PluginHost-B8kxLOeb.mjs.map +0 -1
  48. package/dist/PluginLifecycle-AhQ46NIr.mjs.map +0 -1
  49. package/dist/phases-BauKJOTT.mjs +0 -31
  50. package/dist/phases-BauKJOTT.mjs.map +0 -1
  51. package/dist/publishDefault-D3dAyzZ9.mjs +0 -7
  52. package/dist/publishDefault-D3dAyzZ9.mjs.map +0 -1
  53. package/dist/subscribers-Ce3SWy23.mjs +0 -1913
  54. package/dist/subscribers-Ce3SWy23.mjs.map +0 -1
@@ -1,8 +1,9 @@
1
- import { i as Bus, n as StartupPhase, r as CoreBase, t as ShutdownPhase } from "./phases-FTM69J3a.mjs";
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?: ShutdownPhase;
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: ShutdownPhase;
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(): Promise<void>;
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
- * Your constructor takes the host first and passes it to `super()`. Add your own parameters after
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(): Promise<void>;
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?(): Promise<void>;
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?(): Promise<void>;
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): Promise<void>;
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
- abstract readonly rateLimiter: IRateLimiter;
258
- abstract readonly bus: Bus;
259
- constructor(transport: BotT, runtime: BotRt);
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
- * Your constructor takes `CoreBase` as its first parameter (you don't need to pass it though).
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, Transport as d, PluginLifecycleSpec as f, sealAttachments as i, PluginCtor as l, PluginHost as n, Initializeable as o, attachmentsOf as r, Plugin as s, Attachment as t, PluginOptions as u };
297
- //# sourceMappingURL=PluginHost-DB4j8unu.d.mts.map
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