chapterjs 0.2.2 → 0.3.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/cli/cli.js +41 -6
  4. package/dist/cli/cluster.d.ts +3 -0
  5. package/dist/cli/cluster.js +4 -1
  6. package/dist/cli/dev.js +2 -0
  7. package/dist/cli/project.d.ts +38 -0
  8. package/dist/cli/project.js +51 -10
  9. package/dist/cli/start.js +18 -0
  10. package/dist/cli/test.d.ts +29 -0
  11. package/dist/cli/test.js +444 -0
  12. package/dist/commands/autocomplete.d.ts +1 -1
  13. package/dist/commands/autocomplete.js +37 -41
  14. package/dist/commands/router.d.ts +4 -1
  15. package/dist/commands/router.js +16 -17
  16. package/dist/components/router.d.ts +7 -2
  17. package/dist/components/router.js +17 -16
  18. package/dist/discord/types/channel.d.ts +9 -1
  19. package/dist/discord/types/guild.d.ts +5 -5
  20. package/dist/discord/types/permissions.d.ts +6 -0
  21. package/dist/discord/types/poll.d.ts +7 -1
  22. package/dist/events/router.d.ts +12 -4
  23. package/dist/events/router.js +29 -12
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +1 -0
  26. package/dist/interactions/dispatch.d.ts +7 -1
  27. package/dist/interactions/dispatch.js +7 -2
  28. package/dist/loader/hot.js +22 -2
  29. package/dist/loader/loader.d.ts +7 -0
  30. package/dist/loader/loader.js +22 -12
  31. package/dist/store/backend.d.ts +33 -0
  32. package/dist/store/backend.js +27 -0
  33. package/dist/store/declaration.d.ts +15 -0
  34. package/dist/store/declaration.js +53 -0
  35. package/dist/store/file-backend.d.ts +22 -0
  36. package/dist/store/file-backend.js +172 -0
  37. package/dist/store/ipc-backend.d.ts +53 -0
  38. package/dist/store/ipc-backend.js +87 -0
  39. package/dist/store/memory-backend.d.ts +15 -0
  40. package/dist/store/memory-backend.js +56 -0
  41. package/dist/store/scope.d.ts +8 -0
  42. package/dist/store/scope.js +20 -0
  43. package/dist/store/store.d.ts +115 -0
  44. package/dist/store/store.js +203 -0
  45. package/dist/tasks/scheduler.d.ts +6 -0
  46. package/dist/tasks/scheduler.js +10 -3
  47. package/dist/testing/failure.d.ts +9 -0
  48. package/dist/testing/failure.js +20 -0
  49. package/dist/testing/fake-discord.d.ts +59 -0
  50. package/dist/testing/fake-discord.js +120 -0
  51. package/dist/testing/fake-gateway.d.ts +59 -0
  52. package/dist/testing/fake-gateway.js +123 -0
  53. package/dist/testing/harness.d.ts +387 -0
  54. package/dist/testing/harness.js +1418 -0
  55. package/dist/testing/index.d.ts +4 -0
  56. package/dist/testing/index.js +5 -0
  57. package/dist/testing/scenario.d.ts +39 -0
  58. package/dist/testing/scenario.js +58 -0
  59. package/dist/testing/smoke.d.ts +20 -0
  60. package/dist/testing/smoke.js +659 -0
  61. package/dist/testing/websocket.d.ts +34 -0
  62. package/dist/testing/websocket.js +167 -0
  63. package/dist/testing/world.d.ts +145 -0
  64. package/dist/testing/world.js +1479 -0
  65. package/package.json +20 -3
@@ -0,0 +1,53 @@
1
+ // What a file declares with `store()`, checked when it loads, so a wrong
2
+ // scope or expiration is explained before anything is kept; the store is
3
+ // then bound to the name of its export, which names its data.
4
+ import { DURATION_EXAMPLE, parseDuration } from '../util/duration.js';
5
+ import { bindStore, isStoreDeclaration, storeStateOf, } from './store.js';
6
+ const fail = (message) => {
7
+ throw new TypeError(message);
8
+ };
9
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
10
+ const KEYS = ['scope', 'expires'];
11
+ const SCOPES = ['guild', 'global'];
12
+ /** What a name can be: a file in `data/` is called after it. */
13
+ const NAME = /^[\w.-]+$/;
14
+ /**
15
+ * A store, declared anywhere in `src/` with `store()` and exported: the
16
+ * name of the export is the name of the store.
17
+ */
18
+ export const storeDeclaration = {
19
+ one: 'store',
20
+ many: 'stores',
21
+ is: isStoreDeclaration,
22
+ list: false,
23
+ read(value, { name }) {
24
+ const config = storeStateOf(value)?.config;
25
+ if (!isRecord(config)) {
26
+ return fail(`store() takes an object, or nothing: store<Warning[]>({ expires: '30d' }).`);
27
+ }
28
+ for (const key of Object.keys(config)) {
29
+ if (!KEYS.includes(key)) {
30
+ fail(`"${key}" is not something a store has. It can have: ${KEYS.join(', ')}.`);
31
+ }
32
+ }
33
+ if (!NAME.test(name)) {
34
+ fail(`"${name}" can't name a store: the name of the export is the name of the store and of its file in data/, made of letters, digits, _ . and -.`);
35
+ }
36
+ const scope = (config.scope ?? 'guild');
37
+ if (!SCOPES.includes(scope)) {
38
+ fail(`"scope" is 'guild' (the data is kept per server, the default) or 'global' (one set of data for the whole bot), got ${JSON.stringify(config.scope)}.`);
39
+ }
40
+ let expires = null;
41
+ if (config.expires !== undefined) {
42
+ const ms = typeof config.expires === 'string'
43
+ ? parseDuration(config.expires)
44
+ : null;
45
+ if (ms === null || ms < 1000) {
46
+ fail(`"expires" is how long a value is kept: ${DURATION_EXAMPLE}, 1 s at least. Got ${JSON.stringify(config.expires)}.`);
47
+ }
48
+ expires = ms;
49
+ }
50
+ bindStore(value, { name, scope, expires });
51
+ return Object.freeze({ name, scope, expires });
52
+ },
53
+ };
@@ -0,0 +1,22 @@
1
+ import { type StoreBackend, type StoreEntry, type StoreKeyParts, type StoreListedEntry } from './backend.js';
2
+ /** The folder of a project where the stores keep their files. */
3
+ export declare const DATA_FOLDER = "data";
4
+ export interface FileStoreBackendOptions {
5
+ /** How long a change waits for others before the file is written. */
6
+ writeDelay?: number;
7
+ /** How often expired entries are dropped. */
8
+ sweepEvery?: number;
9
+ now?: () => number;
10
+ }
11
+ /** The data of every store, one file each, loaded on first use. */
12
+ export declare class FileStoreBackend implements StoreBackend {
13
+ #private;
14
+ constructor(dir: string, options?: FileStoreBackendOptions);
15
+ /** The file of a store. */
16
+ file(store: string): string;
17
+ get(store: string, key: StoreKeyParts): Promise<StoreEntry | undefined>;
18
+ set(store: string, key: StoreKeyParts, entry: StoreEntry): Promise<void>;
19
+ delete(store: string, key: StoreKeyParts): Promise<boolean>;
20
+ entries(store: string, prefix: StoreKeyParts): Promise<StoreListedEntry[]>;
21
+ close(): Promise<void>;
22
+ }
@@ -0,0 +1,172 @@
1
+ // The default backend: one JSON file per store in the `data/` folder of
2
+ // the project, kept in memory and written back shortly after each change,
3
+ // atomically (a temporary file, then a rename), so a crash never leaves a
4
+ // half-written file. What is pending when the process exits is written
5
+ // then. Expired entries are dropped when a file is read, when they are
6
+ // asked for, and by a sweep every minute, so a file never grows forever.
7
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { alive, decodeKey, encodeKey, startsWith, } from './backend.js';
10
+ /** The folder of a project where the stores keep their files. */
11
+ export const DATA_FOLDER = 'data';
12
+ /** A name a file can be called: what an export is called. */
13
+ const FILE_NAME = /^[\w.-]+$/;
14
+ /** The data of every store, one file each, loaded on first use. */
15
+ export class FileStoreBackend {
16
+ #dir;
17
+ #stores = new Map();
18
+ #dirty = new Set();
19
+ #now;
20
+ #writeDelay;
21
+ #writeTimer = null;
22
+ #sweeper;
23
+ #onExit;
24
+ constructor(dir, options = {}) {
25
+ this.#dir = dir;
26
+ this.#now = options.now ?? Date.now;
27
+ this.#writeDelay = options.writeDelay ?? 100;
28
+ this.#sweeper = setInterval(() => this.#sweep(), options.sweepEvery ?? 60_000);
29
+ this.#sweeper.unref();
30
+ // What was changed in the last moments is not lost with the process.
31
+ this.#onExit = () => this.#flush();
32
+ process.on('exit', this.#onExit);
33
+ }
34
+ /** The file of a store. */
35
+ file(store) {
36
+ return join(this.#dir, `${store}.json`);
37
+ }
38
+ #load(store) {
39
+ let entries = this.#stores.get(store);
40
+ if (entries)
41
+ return entries;
42
+ if (!FILE_NAME.test(store)) {
43
+ throw new TypeError(`"${store}" can't name a store: its name is the name of its export, made of letters, digits, _ . and -.`);
44
+ }
45
+ entries = new Map();
46
+ let text = null;
47
+ try {
48
+ text = readFileSync(this.file(store), 'utf8');
49
+ }
50
+ catch (error) {
51
+ if (error.code !== 'ENOENT')
52
+ throw error;
53
+ }
54
+ if (text !== null) {
55
+ let parsed;
56
+ try {
57
+ parsed = JSON.parse(text);
58
+ if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
59
+ throw new Error('not a store file');
60
+ }
61
+ }
62
+ catch {
63
+ throw new Error(`${this.file(store)} is not a file ChapterJS wrote: fix it, or delete it to start the store empty.`);
64
+ }
65
+ const now = this.#now();
66
+ for (const [key, value, expiresAt] of parsed.entries) {
67
+ const entry = { value, expiresAt };
68
+ if (alive(entry, now))
69
+ entries.set(encodeKey(key), entry);
70
+ }
71
+ }
72
+ this.#stores.set(store, entries);
73
+ return entries;
74
+ }
75
+ #changed(store) {
76
+ this.#dirty.add(store);
77
+ if (this.#writeTimer)
78
+ return;
79
+ this.#writeTimer = setTimeout(() => this.#flush(), this.#writeDelay);
80
+ this.#writeTimer.unref();
81
+ }
82
+ /** Writes every store that changed. */
83
+ #flush() {
84
+ if (this.#writeTimer) {
85
+ clearTimeout(this.#writeTimer);
86
+ this.#writeTimer = null;
87
+ }
88
+ if (this.#dirty.size === 0)
89
+ return;
90
+ mkdirSync(this.#dir, { recursive: true });
91
+ for (const store of this.#dirty) {
92
+ const entries = this.#stores.get(store);
93
+ if (!entries)
94
+ continue;
95
+ const file = {
96
+ version: 1,
97
+ entries: [...entries].map(([key, { value, expiresAt }]) => [
98
+ decodeKey(key),
99
+ value,
100
+ expiresAt,
101
+ ]),
102
+ };
103
+ const path = this.file(store);
104
+ const temporary = `${path}.${process.pid}.tmp`;
105
+ writeFileSync(temporary, `${JSON.stringify(file)}\n`);
106
+ renameSync(temporary, path);
107
+ }
108
+ this.#dirty.clear();
109
+ }
110
+ #sweep() {
111
+ const now = this.#now();
112
+ for (const [store, entries] of this.#stores) {
113
+ let dropped = false;
114
+ for (const [key, entry] of entries) {
115
+ if (!alive(entry, now)) {
116
+ entries.delete(key);
117
+ dropped = true;
118
+ }
119
+ }
120
+ if (dropped)
121
+ this.#changed(store);
122
+ }
123
+ }
124
+ async get(store, key) {
125
+ const entries = this.#load(store);
126
+ const encoded = encodeKey(key);
127
+ const entry = entries.get(encoded);
128
+ if (!entry)
129
+ return undefined;
130
+ if (alive(entry, this.#now()))
131
+ return entry;
132
+ entries.delete(encoded);
133
+ this.#changed(store);
134
+ return undefined;
135
+ }
136
+ async set(store, key, entry) {
137
+ this.#load(store).set(encodeKey(key), entry);
138
+ this.#changed(store);
139
+ }
140
+ async delete(store, key) {
141
+ const entries = this.#load(store);
142
+ const encoded = encodeKey(key);
143
+ const entry = entries.get(encoded);
144
+ if (!entry)
145
+ return false;
146
+ entries.delete(encoded);
147
+ this.#changed(store);
148
+ return alive(entry, this.#now());
149
+ }
150
+ async entries(store, prefix) {
151
+ const entries = this.#load(store);
152
+ const now = this.#now();
153
+ const listed = [];
154
+ for (const [encoded, entry] of entries) {
155
+ const key = decodeKey(encoded);
156
+ if (!startsWith(key, prefix))
157
+ continue;
158
+ if (alive(entry, now))
159
+ listed.push({ key, ...entry });
160
+ else {
161
+ entries.delete(encoded);
162
+ this.#changed(store);
163
+ }
164
+ }
165
+ return listed;
166
+ }
167
+ async close() {
168
+ clearInterval(this.#sweeper);
169
+ process.off('exit', this.#onExit);
170
+ this.#flush();
171
+ }
172
+ }
@@ -0,0 +1,53 @@
1
+ import type { StoreBackend, StoreEntry, StoreKeyParts, StoreListedEntry } from './backend.js';
2
+ /** What a process asks the first one. */
3
+ export type StoreRequest = {
4
+ type: 'store';
5
+ id: number;
6
+ } & ({
7
+ op: 'get';
8
+ store: string;
9
+ key: StoreKeyParts;
10
+ } | {
11
+ op: 'set';
12
+ store: string;
13
+ key: StoreKeyParts;
14
+ entry: StoreEntry;
15
+ } | {
16
+ op: 'delete';
17
+ store: string;
18
+ key: StoreKeyParts;
19
+ } | {
20
+ op: 'entries';
21
+ store: string;
22
+ prefix: StoreKeyParts;
23
+ });
24
+ /** What the first process answers. */
25
+ export interface StoreReply {
26
+ type: 'store';
27
+ id: number;
28
+ result?: unknown;
29
+ error?: string;
30
+ }
31
+ /** The two ends of a channel, as a process and its child have them. */
32
+ export interface Channel<Out, In> {
33
+ send(message: Out): void;
34
+ on(listener: (message: In) => void): void;
35
+ }
36
+ export declare const isStoreRequest: (message: unknown) => message is StoreRequest;
37
+ export declare const isStoreReply: (message: unknown) => message is StoreReply;
38
+ /** The backend of a process that is not the first: every call is a message. */
39
+ export declare class IpcStoreBackend implements StoreBackend {
40
+ #private;
41
+ constructor(channel: Channel<StoreRequest, unknown>);
42
+ get(store: string, key: StoreKeyParts): Promise<StoreEntry | undefined>;
43
+ set(store: string, key: StoreKeyParts, entry: StoreEntry): Promise<void>;
44
+ delete(store: string, key: StoreKeyParts): Promise<boolean>;
45
+ entries(store: string, prefix: StoreKeyParts): Promise<StoreListedEntry[]>;
46
+ close(): Promise<void>;
47
+ }
48
+ /**
49
+ * In the first process: answers the requests of one other process with
50
+ * the real backend. Returns what to call with each message received from
51
+ * that process; a message that is not a request is ignored.
52
+ */
53
+ export declare function serveStores(backend: StoreBackend, reply: (message: StoreReply) => void): (message: unknown) => void;
@@ -0,0 +1,87 @@
1
+ // A bot split over several processes has one copy of its data: the first
2
+ // process keeps it (with the file backend) and the others ask it over
3
+ // IPC, the channel they already share with it. Each process then sees the
4
+ // same thing, whichever shard a command was used on.
5
+ export const isStoreRequest = (message) => typeof message === 'object' &&
6
+ message !== null &&
7
+ message.type === 'store' &&
8
+ 'op' in message;
9
+ export const isStoreReply = (message) => typeof message === 'object' &&
10
+ message !== null &&
11
+ message.type === 'store' &&
12
+ !('op' in message);
13
+ /** The backend of a process that is not the first: every call is a message. */
14
+ export class IpcStoreBackend {
15
+ #channel;
16
+ #waiting = new Map();
17
+ #lastId = 0;
18
+ constructor(channel) {
19
+ this.#channel = channel;
20
+ channel.on(message => {
21
+ if (!isStoreReply(message))
22
+ return;
23
+ const pending = this.#waiting.get(message.id);
24
+ if (!pending)
25
+ return;
26
+ this.#waiting.delete(message.id);
27
+ if (message.error !== undefined)
28
+ pending.reject(new Error(message.error));
29
+ else
30
+ pending.resolve(message.result);
31
+ });
32
+ }
33
+ #ask(request) {
34
+ const id = ++this.#lastId;
35
+ return new Promise((resolve, reject) => {
36
+ this.#waiting.set(id, {
37
+ resolve: resolve,
38
+ reject,
39
+ });
40
+ this.#channel.send({ type: 'store', id, ...request });
41
+ });
42
+ }
43
+ get(store, key) {
44
+ return this.#ask({ op: 'get', store, key });
45
+ }
46
+ set(store, key, entry) {
47
+ return this.#ask({ op: 'set', store, key, entry });
48
+ }
49
+ delete(store, key) {
50
+ return this.#ask({ op: 'delete', store, key });
51
+ }
52
+ entries(store, prefix) {
53
+ return this.#ask({ op: 'entries', store, prefix });
54
+ }
55
+ async close() {
56
+ // Nothing is kept here: the first process writes.
57
+ }
58
+ }
59
+ /**
60
+ * In the first process: answers the requests of one other process with
61
+ * the real backend. Returns what to call with each message received from
62
+ * that process; a message that is not a request is ignored.
63
+ */
64
+ export function serveStores(backend, reply) {
65
+ return message => {
66
+ if (!isStoreRequest(message))
67
+ return;
68
+ const { id } = message;
69
+ const run = () => {
70
+ switch (message.op) {
71
+ case 'get':
72
+ return backend.get(message.store, message.key);
73
+ case 'set':
74
+ return backend.set(message.store, message.key, message.entry);
75
+ case 'delete':
76
+ return backend.delete(message.store, message.key);
77
+ case 'entries':
78
+ return backend.entries(message.store, message.prefix);
79
+ }
80
+ };
81
+ run().then(result => reply({ type: 'store', id, result }), (error) => reply({
82
+ type: 'store',
83
+ id,
84
+ error: error instanceof Error ? error.message : String(error),
85
+ }));
86
+ };
87
+ }
@@ -0,0 +1,15 @@
1
+ import { type StoreBackend, type StoreEntry, type StoreKeyParts, type StoreListedEntry } from './backend.js';
2
+ /** The data of every store, in memory, forgotten with the process. */
3
+ export declare class MemoryStoreBackend implements StoreBackend {
4
+ #private;
5
+ constructor(options?: {
6
+ now?: () => number;
7
+ });
8
+ get(store: string, key: StoreKeyParts): Promise<StoreEntry | undefined>;
9
+ set(store: string, key: StoreKeyParts, entry: StoreEntry): Promise<void>;
10
+ delete(store: string, key: StoreKeyParts): Promise<boolean>;
11
+ entries(store: string, prefix: StoreKeyParts): Promise<StoreListedEntry[]>;
12
+ /** Forgets everything: what a test does with its stores stays in it. */
13
+ clear(): void;
14
+ close(): Promise<void>;
15
+ }
@@ -0,0 +1,56 @@
1
+ // A backend that keeps the data of the stores in memory and nowhere else:
2
+ // what `chapterjs test` gives a bot under test, so a test never touches
3
+ // the `data/` folder of the project and starts from nothing every time.
4
+ import { alive, decodeKey, encodeKey, startsWith, } from './backend.js';
5
+ /** The data of every store, in memory, forgotten with the process. */
6
+ export class MemoryStoreBackend {
7
+ #stores = new Map();
8
+ #now;
9
+ constructor(options = {}) {
10
+ this.#now = options.now ?? Date.now;
11
+ }
12
+ #store(name) {
13
+ let store = this.#stores.get(name);
14
+ if (!store) {
15
+ store = new Map();
16
+ this.#stores.set(name, store);
17
+ }
18
+ return store;
19
+ }
20
+ async get(store, key) {
21
+ const entry = this.#store(store).get(encodeKey(key));
22
+ if (!entry)
23
+ return undefined;
24
+ if (!alive(entry, this.#now())) {
25
+ this.#store(store).delete(encodeKey(key));
26
+ return undefined;
27
+ }
28
+ return entry;
29
+ }
30
+ async set(store, key, entry) {
31
+ this.#store(store).set(encodeKey(key), entry);
32
+ }
33
+ async delete(store, key) {
34
+ return this.#store(store).delete(encodeKey(key));
35
+ }
36
+ async entries(store, prefix) {
37
+ const now = this.#now();
38
+ const result = [];
39
+ for (const [encoded, entry] of this.#store(store)) {
40
+ const key = decodeKey(encoded);
41
+ if (!startsWith(key, prefix))
42
+ continue;
43
+ if (!alive(entry, now)) {
44
+ this.#store(store).delete(encoded);
45
+ continue;
46
+ }
47
+ result.push({ key, ...entry });
48
+ }
49
+ return result;
50
+ }
51
+ /** Forgets everything: what a test does with its stores stays in it. */
52
+ clear() {
53
+ this.#stores.clear();
54
+ }
55
+ async close() { }
56
+ }
@@ -0,0 +1,8 @@
1
+ /** Runs `fn` for a server (or for none, with `null`). */
2
+ export declare function withGuild<T>(guildId: string | null, fn: () => T): T;
3
+ /**
4
+ * The id of the server the current function runs for: `null` when it runs
5
+ * for none (a task, a private message), `undefined` outside any function
6
+ * the framework runs.
7
+ */
8
+ export declare function currentGuildId(): string | null | undefined;
@@ -0,0 +1,20 @@
1
+ // The server the code runs for, known to the framework and not written by
2
+ // the user: a store per server reads it, so `warnings.get(member.id)` in a
3
+ // command is the warnings of the server the command was used in. Set by
4
+ // the routers around every function they run, on an asynchronous context
5
+ // of Node, so it follows the `await`s into the files the function imports.
6
+ import { AsyncLocalStorage } from 'node:async_hooks';
7
+ /** The id of the server the current function runs for, or `null` for none. */
8
+ const scope = new AsyncLocalStorage();
9
+ /** Runs `fn` for a server (or for none, with `null`). */
10
+ export function withGuild(guildId, fn) {
11
+ return scope.run(guildId, fn);
12
+ }
13
+ /**
14
+ * The id of the server the current function runs for: `null` when it runs
15
+ * for none (a task, a private message), `undefined` outside any function
16
+ * the framework runs.
17
+ */
18
+ export function currentGuildId() {
19
+ return scope.getStore();
20
+ }
@@ -0,0 +1,115 @@
1
+ import type { Duration } from '../commands/command.js';
2
+ import type { Guild } from '../structures/guild.js';
3
+ /** Where the data of a store is kept apart: per server, or one for the whole bot. */
4
+ export type StoreScope = 'guild' | 'global';
5
+ /** What a file gives to `store()`. */
6
+ export interface StoreConfig {
7
+ /**
8
+ * `'guild'` (the default): the data is kept per server, and a function
9
+ * that runs in a server reads and writes the data of that server, with
10
+ * nothing to say. `'global'`: one set of data for the whole bot.
11
+ */
12
+ scope?: StoreScope;
13
+ /**
14
+ * How long a value is kept, unless `set()` says otherwise: a number and
15
+ * a unit, `s`, `m`, `h` or `d`, like `'10m'`, `'1d'` or `'1h30m'`. Kept
16
+ * forever by default.
17
+ */
18
+ expires?: Duration;
19
+ }
20
+ /**
21
+ * A key of a store: a text, or a list of texts for a key made of several
22
+ * parts (`[channel.id, message.id]`), which `entries()` can then list by
23
+ * prefix.
24
+ */
25
+ export type StoreKey = string | readonly string[];
26
+ /** What `set()` and `update()` take besides the value. */
27
+ export interface StoreSetOptions {
28
+ /**
29
+ * How long the value is kept: a duration like `'10m'`, a `Date`, or
30
+ * `null` to keep it forever. The `expires` of the store by default.
31
+ */
32
+ expires?: Duration | Date | null;
33
+ }
34
+ /** One entry of a store, as `entries()` lists them. */
35
+ export interface StoreEntry<T> {
36
+ /** The key, every part of it (the server left out for a store per server). */
37
+ key: readonly string[];
38
+ value: T;
39
+ /** When the value expires, or `null` when it is kept forever. */
40
+ expires: Date | null;
41
+ }
42
+ /** A store, read and written for one server or for the whole bot. */
43
+ export interface ScopedStore<T> {
44
+ /** The value at `key`, or `undefined` when there is none, or it expired. */
45
+ get(key: StoreKey): Promise<T | undefined>;
46
+ /** Keeps `value` at `key`. The value must be JSON: no `Date`, `Map` or class instance. */
47
+ set(key: StoreKey, value: T, options?: StoreSetOptions): Promise<void>;
48
+ /**
49
+ * Reads the value at `key` and keeps what `change` returns, in one go.
50
+ * `change` receives `undefined` when there is no value yet. The
51
+ * expiration stays what it was, unless `options` say otherwise.
52
+ */
53
+ update(key: StoreKey, change: (current: T | undefined) => T, options?: StoreSetOptions): Promise<T>;
54
+ /** Forgets the value at `key`. `true` when there was one. */
55
+ delete(key: StoreKey): Promise<boolean>;
56
+ /**
57
+ * Every entry, or the ones whose key starts with `prefix`, in no
58
+ * particular order. Expired ones are left out.
59
+ */
60
+ entries(prefix?: StoreKey): Promise<StoreEntry<T>[]>;
61
+ /**
62
+ * How long the value at `key` is still kept, in milliseconds: `null`
63
+ * when it is kept forever, `undefined` when there is no value.
64
+ */
65
+ ttl(key: StoreKey): Promise<number | null | undefined>;
66
+ }
67
+ /** What `store()` returns: a declaration the framework finds in the exports of a file. */
68
+ export interface Store<T> extends ScopedStore<T> {
69
+ /**
70
+ * The same store, for one server: what a task, or a function that runs
71
+ * outside a server, uses to read and write the data of that server.
72
+ */
73
+ in(guild: Guild | string): ScopedStore<T>;
74
+ }
75
+ /** A store as the loader knows it: its config, and once loaded, its name. */
76
+ export interface StoreState {
77
+ readonly config: unknown;
78
+ /** The name of the export that declares it, once bound by the loader. */
79
+ name?: string;
80
+ /** What `read` found in the config. */
81
+ scope: StoreScope;
82
+ /** The default expiration, in ms, or `null` for never. */
83
+ expires: number | null;
84
+ }
85
+ /** The state of a declared store, if the value is one. */
86
+ export declare function storeStateOf(value: unknown): StoreState | undefined;
87
+ export declare const isStoreDeclaration: (value: unknown) => boolean;
88
+ /** Binds a declared store to the name of its export. */
89
+ export declare function bindStore(declared: unknown, bound: {
90
+ name: string;
91
+ scope: StoreScope;
92
+ expires: number | null;
93
+ }): void;
94
+ /**
95
+ * Refuses what would not survive a trip through JSON: a `Date` would come
96
+ * back as a text, a `Map` as `{}`, a class instance as a plain object.
97
+ */
98
+ export declare function checkJson(value: unknown, path?: string): void;
99
+ /**
100
+ * Declares a store: data the bot keeps from one restart to the next,
101
+ * typed by what it holds. Export the result from any file of `src/`: the
102
+ * name of the export is the name of the store, and its file in `data/`.
103
+ * Per server unless `scope: 'global'` is given, with an expiration if
104
+ * `expires` is.
105
+ *
106
+ * ```ts
107
+ * import { store } from 'chapterjs';
108
+ *
109
+ * export const warnings = store<Warning[]>({ expires: '30d' });
110
+ *
111
+ * // In a command, in a server:
112
+ * await warnings.update(member.id, (list = []) => [...list, warning]);
113
+ * ```
114
+ */
115
+ export declare function store<T>(config?: StoreConfig): Store<T>;