spearkit 0.3.0 → 0.4.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.
@@ -0,0 +1,42 @@
1
+ # Graceful shutdown
2
+
3
+ A `Ctrl-C` or a container stop sends your process a signal. If you don't handle
4
+ it, the process dies mid-flight — the gateway connection, scheduler timers, and
5
+ any open database handles are reaped abruptly. Graceful shutdown runs an optional
6
+ cleanup hook, calls `client.destroy()` (which also stops spearkit's scheduler),
7
+ then exits — with a hard timeout so a wedged shutdown can't hang forever.
8
+
9
+ ## On a SpearClient
10
+
11
+ ```ts
12
+ client.enableGracefulShutdown({
13
+ onShutdown: () => db.close(), // flush state before we exit
14
+ });
15
+ await client.start();
16
+ ```
17
+
18
+ Progress is logged through `client.logger`. The method returns a disposer that
19
+ removes the signal handlers (useful for tests / hot-reload).
20
+
21
+ ## Standalone
22
+
23
+ `gracefulShutdown(client, options)` works with any object that has a `destroy()`
24
+ method:
25
+
26
+ ```ts
27
+ import { gracefulShutdown } from "spearkit";
28
+
29
+ gracefulShutdown(client, { onShutdown: () => db.close() });
30
+ ```
31
+
32
+ ## Options
33
+
34
+ | Field | Default | Meaning |
35
+ | --- | --- | --- |
36
+ | `signals` | `["SIGINT", "SIGTERM"]` | Signals to listen for. |
37
+ | `timeoutMs` | `10000` | Force-exit if shutdown exceeds this. |
38
+ | `onShutdown` | — | Runs before `destroy()`; receives the signal. |
39
+ | `exit` | `true` | Call `process.exit()` when done (set `false` in tests). |
40
+ | `logger` | — | `{ info?, error? }` progress logger. |
41
+
42
+ Shutdown runs **once** — repeated signals during teardown are ignored.
package/docs/store.md ADDED
@@ -0,0 +1,90 @@
1
+ # Key-value store & settings
2
+
3
+ Almost every community bot needs to remember *something* per guild — a custom
4
+ prefix, a mod-log channel, a welcome message — and reaches for a database on day
5
+ one. spearkit ships a dependency-free `KeyValueStore` interface with two
6
+ backends, plus a typed per-guild settings helper. Swap in Redis/SQL later by
7
+ implementing the same interface.
8
+
9
+ ## Stores
10
+
11
+ ```ts
12
+ import { JsonStore, MemoryStore } from "spearkit";
13
+
14
+ const dev = new MemoryStore(); // in-memory, great for tests
15
+ const prod = new JsonStore("data/db.json"); // durable JSON file
16
+ ```
17
+
18
+ Both implement `KeyValueStore`:
19
+
20
+ ```ts
21
+ await store.set("key", { any: "json" });
22
+ await store.get<{ any: string }>("key"); // typed read, or undefined
23
+ await store.has("key");
24
+ await store.delete("key"); // → boolean (existed?)
25
+ await store.keys(); // → string[]
26
+ await store.clear();
27
+ ```
28
+
29
+ `MemoryStore` deep-clones on read and write, so callers can't mutate stored
30
+ state. `JsonStore` serves reads from an in-memory cache and commits writes
31
+ atomically (temp file + rename) through a queue — a crash mid-write can't corrupt
32
+ the file, and concurrent writes don't interleave.
33
+
34
+ ## Typed per-guild settings
35
+
36
+ `createSettings` wraps a store with defaults. `get` always returns a complete
37
+ object; `set` persists *only* the overrides, so widening `defaults` later is
38
+ safe.
39
+
40
+ ```ts
41
+ import { JsonStore, createSettings } from "spearkit";
42
+
43
+ const settings = createSettings({
44
+ store: new JsonStore("data/guilds.json"),
45
+ defaults: { prefix: "!", modLogChannelId: null as string | null },
46
+ });
47
+
48
+ const cfg = await settings.get(guildId); // { prefix, modLogChannelId }
49
+ await settings.set(guildId, { prefix: "?" }); // shallow-merged + persisted
50
+ await settings.reset(guildId); // back to defaults
51
+ ```
52
+
53
+ Pass `namespace` to keep several settings groups in one store:
54
+
55
+ ```ts
56
+ const guilds = createSettings({ store, defaults: { prefix: "!" }, namespace: "guild" });
57
+ const users = createSettings({ store, defaults: { xp: 0 }, namespace: "user" });
58
+ ```
59
+
60
+ ## Dynamic per-guild prefix
61
+
62
+ A stored prefix is only useful if prefix commands respect it. `prefix.dynamic`
63
+ resolves extra prefix(es) per message — combine it with `createSettings` for true
64
+ per-guild prefixes:
65
+
66
+ ```ts
67
+ const client = new SpearClient({
68
+ prefix: {
69
+ dynamic: async (message) =>
70
+ message.guildId ? (await settings.get(message.guildId)).prefix : null,
71
+ },
72
+ });
73
+ ```
74
+
75
+ The resolver runs on every candidate message, so keep it fast (cache or use the
76
+ in-memory `JsonStore` cache). Returned prefixes are tried *in addition* to any
77
+ static `prefix`. See [Prefix commands](./prefix.md) for the rest of the prefix
78
+ system.
79
+
80
+ ## Namespacing a raw store
81
+
82
+ `namespaced(store, prefix)` returns a `KeyValueStore` whose keys are
83
+ transparently prefixed — handy for sharing one file across features:
84
+
85
+ ```ts
86
+ import { namespaced } from "spearkit";
87
+
88
+ const tags = namespaced(store, "tags");
89
+ await tags.set("hello", "world"); // stored under "tags:hello"
90
+ ```
package/docs/usage.md ADDED
@@ -0,0 +1,188 @@
1
+ # Usage tracking
2
+
3
+ Usage tracking records **who used what**: every command, component, context-menu
4
+ and prefix-command invocation — successful or errored — becomes a `UsageEvent`
5
+ that spearkit can persist to a
6
+ store and/or mirror into a Discord channel. Turn it on with the client's `usage`
7
+ option.
8
+
9
+ ## Usage tracking vs the logger
10
+
11
+ These look similar but answer different questions, and they are completely
12
+ independent sinks:
13
+
14
+ | | Logger | Usage tracking |
15
+ | --- | --- | --- |
16
+ | Question | *What is the bot doing?* (diagnostics) | *Who used which feature?* (audit) |
17
+ | Content | Free-form messages, levels, errors, internals | Structured `UsageEvent`s for every completed use (with its `outcome`) |
18
+ | Sinks | Console / your log pipeline | A database store and/or a Discord channel |
19
+ | Configured by | the `logger` option | the `usage` option |
20
+
21
+ Both successes and handler errors are recorded as usage events — an error carries
22
+ `outcome: "error"` and an `errorMessage` — so usage is a complete audit trail.
23
+ The [logger](./logging.md) is for debugging; usage tracking is for analytics,
24
+ audit trails, and "top commands" dashboards.
25
+
26
+ ## Enabling it
27
+
28
+ Set the `usage` option. Provide a `store` (a database), a `channel` (a Discord
29
+ channel id to mirror events into), or both:
30
+
31
+ ```ts
32
+ import { MemoryUsageStore, SpearClient } from "spearkit";
33
+
34
+ const client = new SpearClient({
35
+ usage: {
36
+ store: new MemoryUsageStore(),
37
+ channel: "123456789012345678", // optional: also post each event here
38
+ },
39
+ });
40
+ ```
41
+
42
+ Once enabled, spearkit auto-tracks every command, component, context-menu and
43
+ prefix-command invocation — successes and errors alike — with no tracking code in
44
+ your handlers.
45
+
46
+ ## The usage event
47
+
48
+ Each tracked use is a `UsageEvent`:
49
+
50
+ ```ts
51
+ interface UsageEvent {
52
+ type: UsageType; // "command" | "prefix" | "component" | "event"
53
+ name: string; // command / component / event name
54
+ userId?: string;
55
+ userTag?: string;
56
+ guildId?: string | null;
57
+ channelId?: string | null;
58
+ detail?: string; // free-form extra detail
59
+ outcome?: UsageOutcome; // "success" | "error"
60
+ durationMs?: number; // handler wall-clock time
61
+ options?: Readonly<Record<string, UsageMetaValue>>; // snapshot of typed options
62
+ errorMessage?: string; // set when outcome === "error"
63
+ timestamp: Date;
64
+ }
65
+ type UsageType = "command" | "prefix" | "component" | "event";
66
+ type UsageOutcome = "success" | "error";
67
+ type UsageMetaValue = string | number | boolean | null;
68
+ ```
69
+
70
+ ## Stores (the database)
71
+
72
+ A store is any object implementing `UsageStore`:
73
+
74
+ ```ts
75
+ interface UsageStore {
76
+ record(event: UsageEvent): void | Promise<void>;
77
+ all(): UsageEvent[] | Promise<readonly UsageEvent[]>;
78
+ }
79
+ ```
80
+
81
+ spearkit ships two.
82
+
83
+ ### MemoryUsageStore
84
+
85
+ In-memory and synchronous — ideal for tests, prototypes, and live dashboards.
86
+ Pass an optional cap to keep only the most recent N events:
87
+
88
+ ```ts
89
+ import { MemoryUsageStore } from "spearkit";
90
+
91
+ const store = new MemoryUsageStore(1_000); // keep the last 1,000 events
92
+
93
+ store.all(); // readonly UsageEvent[]
94
+ store.size; // number of events held
95
+ store.byUser("123..."); // UsageEvent[] for one user id
96
+ store.clear(); // forget everything
97
+ ```
98
+
99
+ ### JsonFileUsageStore
100
+
101
+ Durable and dependency-free: appends one event per line as newline-delimited
102
+ JSON (`.jsonl`). `all()` reads the file back and parses it, so it behaves like a
103
+ small file-backed database:
104
+
105
+ ```ts
106
+ import { JsonFileUsageStore } from "spearkit";
107
+
108
+ const store = new JsonFileUsageStore("./usage.jsonl");
109
+
110
+ await store.record({ type: "command", name: "ping", timestamp: new Date() });
111
+ const events = await store.all(); // readonly UsageEvent[]
112
+ ```
113
+
114
+ The directory is created on demand. Because `all()` is async here (it reads from
115
+ disk), always `await` it.
116
+
117
+ ## Querying the store
118
+
119
+ `client.usage.store` is the store you configured — query it directly. Note that
120
+ `all()` may be synchronous (`MemoryUsageStore`) or asynchronous
121
+ (`JsonFileUsageStore`); awaiting works for both:
122
+
123
+ ```ts
124
+ const store = client.usage.store;
125
+ if (store !== undefined) {
126
+ const events = await store.all();
127
+ const topCommand = events.filter((e) => e.type === "command").length;
128
+ console.log(`${topCommand} command uses recorded`);
129
+ }
130
+ ```
131
+
132
+ ## The Discord channel reporter
133
+
134
+ Besides (or instead of) a store, you can mirror each event into a Discord
135
+ channel. Pass `channel` (a channel id) in the `usage` option; spearkit posts one
136
+ line per event using `formatUsage`:
137
+
138
+ ```ts
139
+ import { SpearClient } from "spearkit";
140
+
141
+ new SpearClient({ usage: { channel: "123456789012345678" } });
142
+ ```
143
+
144
+ `formatUsage(event)` is the default renderer (e.g. `` `command` **ping** by
145
+ user#0001 in <#…> ``). Override it with `format` to control the line:
146
+
147
+ ```ts
148
+ import { SpearClient, type UsageEvent } from "spearkit";
149
+
150
+ new SpearClient({
151
+ usage: {
152
+ channel: "123456789012345678",
153
+ format: (event: UsageEvent) => `${event.userTag ?? "someone"} used ${event.name}`,
154
+ },
155
+ });
156
+ ```
157
+
158
+ ## The usage tracker
159
+
160
+ `client.usage` is a `UsageTracker`. The client configures it from the `usage`
161
+ option, but you can drive it directly:
162
+
163
+ | Member | Description |
164
+ | ------ | ----------- |
165
+ | `setStore(store)` | Set (or swap) the persistence store. |
166
+ | `reportTo(channelId, format?)` | Mirror events into a channel, optionally with a custom formatter. |
167
+ | `track(event)` | Record a use. Returns immediately; storing/reporting run in the background. |
168
+ | `store` | The configured store, for querying. |
169
+ | `enabled` | `true` if a store or channel is configured. |
170
+
171
+ ```ts
172
+ import { JsonFileUsageStore } from "spearkit";
173
+
174
+ client.usage.setStore(new JsonFileUsageStore("./usage.jsonl"));
175
+ client.usage.reportTo("123456789012345678");
176
+
177
+ // Record a custom event yourself (e.g. a non-command action).
178
+ client.usage.track({ type: "event", name: "signup", timestamp: new Date() });
179
+ ```
180
+
181
+ Tracking is fire-and-forget: a slow store or channel never blocks command
182
+ handling, and any failure is logged rather than thrown.
183
+
184
+ ## See also
185
+
186
+ - [Logging](./logging.md) — diagnostics, the other sink.
187
+ - [Commands](./commands.md) / [Prefix commands](./prefix.md) / [Components](./components.md) — what gets tracked.
188
+ - [Client](./client.md) — the `usage` option.