spectre.db 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,165 @@
1
+ SPECTRE.DB SOURCE AVAILABLE LICENSE
2
+ Version 1.0 — 2026
3
+
4
+ Copyright (c) 2026 ScarysMonsters (https://github.com/ScarysMonsters)
5
+ All rights reserved on the original work.
6
+
7
+ ================================================================================
8
+ DEFINITIONS
9
+ ================================================================================
10
+
11
+ "Software" refers to the spectre.db source code, documentation, examples,
12
+ configuration files, and any associated materials published in this repository.
13
+
14
+ "Author" refers to ScarysMonsters, the sole original creator and legal owner
15
+ of the Software.
16
+
17
+ "You" refers to any individual or legal entity exercising permissions granted
18
+ by this License.
19
+
20
+ "Derivative Work" means any work that is based on, incorporates, modifies,
21
+ translates, or is otherwise derived from the Software or any substantial part
22
+ of it, regardless of the form in which it is distributed.
23
+
24
+ "Commercial Use" means any use of the Software or a Derivative Work that is
25
+ primarily intended for or directed toward commercial advantage or monetary
26
+ compensation, including but not limited to selling, licensing, or providing
27
+ the Software as a service.
28
+
29
+ ================================================================================
30
+ GRANT OF RIGHTS
31
+ ================================================================================
32
+
33
+ Subject to the conditions and limitations set forth in this License, the Author
34
+ grants You a worldwide, royalty-free, non-exclusive, non-transferable license to:
35
+
36
+ 1. Use the Software for personal, educational, or non-commercial purposes.
37
+
38
+ 2. Copy and redistribute the Software in source or compiled form, provided
39
+ that You comply with the conditions in this License.
40
+
41
+ 3. Modify the Software and create Derivative Works, provided that You comply
42
+ with the conditions in this License.
43
+
44
+ 4. Integrate the Software or Derivative Works into your own projects,
45
+ including Discord bots, automation tools, and other applications, provided
46
+ that such integration complies with the conditions in this License.
47
+
48
+ ================================================================================
49
+ CONDITIONS
50
+ ================================================================================
51
+
52
+ Any exercise of the rights granted above must satisfy all of the following
53
+ conditions:
54
+
55
+ 1. ATTRIBUTION REQUIRED
56
+ All copies, redistributions, or Derivative Works — whether in source or
57
+ binary form — must retain, in a clearly visible location:
58
+
59
+ a. The original copyright notice:
60
+ "Copyright (c) 2025 ScarysMonsters — spectre.db"
61
+
62
+ b. A reference to this License and a link to the original repository:
63
+ https://github.com/ScarysMonsters/spectre.db
64
+
65
+ c. A clear indication of any modifications made to the original Software,
66
+ with a description of what was changed and when.
67
+
68
+ Attribution must not suggest that the Author endorses You or your use of
69
+ the Software.
70
+
71
+ 2. NO OWNERSHIP CLAIM
72
+ You may not represent, claim, imply, or otherwise assert — publicly or
73
+ privately — that You are the original author, creator, or owner of the
74
+ Software or any substantial portion of it. This prohibition applies
75
+ regardless of the extent of your modifications.
76
+
77
+ Publishing a Derivative Work under a different name does not transfer,
78
+ diminish, or otherwise affect the Author's intellectual property rights
79
+ over the original Software.
80
+
81
+ 3. LICENSE PRESERVATION
82
+ All copies, redistributions, and Derivative Works must include a copy of
83
+ this License in full, without modification.
84
+
85
+ 4. NO MISREPRESENTATION OF ORIGIN
86
+ You may not remove, obscure, alter, or replace any copyright notices,
87
+ license headers, authorship statements, or attribution markers present in
88
+ the original Software.
89
+
90
+ 5. COMMERCIAL USE REQUIRES PRIOR WRITTEN CONSENT
91
+ Any Commercial Use of the Software or any Derivative Work requires the
92
+ Author's explicit, prior written authorization. To request commercial
93
+ authorization, contact the Author through the official repository.
94
+
95
+ Unauthorized Commercial Use constitutes a material breach of this License
96
+ and an infringement of the Author's intellectual property rights.
97
+
98
+ ================================================================================
99
+ INTELLECTUAL PROPERTY AND ENFORCEMENT
100
+ ================================================================================
101
+
102
+ The Software, including its architecture, design, logic, and original
103
+ expression, is the exclusive intellectual property of ScarysMonsters.
104
+
105
+ The Author reserves all rights not expressly granted in this License, including
106
+ but not limited to:
107
+
108
+ — The right to revoke this License for any individual or entity found in
109
+ breach of its conditions, effective immediately upon written notice.
110
+
111
+ — The right to issue takedown requests, DMCA notices, or equivalent
112
+ intellectual property enforcement actions against any person, organization,
113
+ platform, or registry (including but not limited to npm, GitHub, GitLab,
114
+ PyPI, or any package registry) distributing the Software or a Derivative
115
+ Work in violation of this License.
116
+
117
+ — The right to pursue civil remedies, including injunctive relief and
118
+ damages, under applicable copyright and intellectual property laws, in any
119
+ jurisdiction where such violations occur.
120
+
121
+ — The right to request the removal or delisting of any project, package,
122
+ repository, or publication that infringes upon the Author's rights under
123
+ this License or applicable law, through administrative, legal, or platform
124
+ enforcement mechanisms.
125
+
126
+ The Author's failure to enforce any provision of this License at any time shall
127
+ not be construed as a waiver of the Author's rights to enforce such provision
128
+ at a later time or in other circumstances.
129
+
130
+ ================================================================================
131
+ DISCLAIMER OF WARRANTIES
132
+ ================================================================================
133
+
134
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
135
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
136
+ FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
137
+
138
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
139
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
140
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
141
+ SOFTWARE.
142
+
143
+ ================================================================================
144
+ TERMINATION
145
+ ================================================================================
146
+
147
+ This License and the rights granted hereunder will terminate automatically and
148
+ immediately upon any breach by You of the conditions set forth in this License.
149
+
150
+ Upon termination, You must cease all use, distribution, and possession of the
151
+ Software and all Derivative Works, and destroy all copies in your control.
152
+
153
+ Termination of this License does not limit the Author's right to pursue
154
+ additional remedies for damages or injunctive relief arising from the breach.
155
+
156
+ ================================================================================
157
+ CONTACT
158
+ ================================================================================
159
+
160
+ For questions regarding this License, commercial use authorization, or to
161
+ report a violation:
162
+
163
+ GitHub: https://github.com/ScarysMonsters
164
+ Repository: https://github.com/ScarysMonsters/spectre.db
165
+ Issues: https://github.com/ScarysMonsters/spectre.db/issues
package/README.md ADDED
@@ -0,0 +1,253 @@
1
+ > [!IMPORTANT]
2
+ > ## Project Status
3
+ >
4
+ > **This project is actively maintained and developed by ScarysMonsters.**
5
+
6
+ > [!NOTE]
7
+ > **spectre.db is a zero-dependency, production-grade file-based JSON database for Node.js bots and small applications.**
8
+ > **Built on a WAL (Write-Ahead Log) architecture with atomic writes, LRU cache, real transactions, and optional AES-256 encryption.**
9
+
10
+ ## About
11
+
12
+ <strong>Welcome to `spectre.db`, a Node.js module that provides a lightweight, persistent key-value database engineered for Discord bots and backend services.</strong>
13
+
14
+ - spectre.db is a [Node.js](https://nodejs.org) module with **zero external dependencies** — powered entirely by Node.js built-ins.
15
+ - Uses a **WAL + snapshot** architecture so your data is never at risk from an unclean shutdown.
16
+
17
+ <div align="center">
18
+ <p>
19
+ <a href="https://www.npmjs.com/package/spectre.db"><img src="https://img.shields.io/npm/v/spectre.db.svg" alt="npm version" /></a>
20
+ <a href="https://www.npmjs.com/package/spectre.db"><img src="https://img.shields.io/npm/dt/spectre.db.svg" alt="npm downloads" /></a>
21
+ <a href="https://github.com/ScarysMonsters/spectre.db"><img src="https://img.shields.io/github/stars/ScarysMonsters/spectre.db?style=flat" alt="GitHub stars" /></a>
22
+ <a href="https://github.com/ScarysMonsters/spectre.db/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/spectre.db.svg" alt="license" /></a>
23
+ </p>
24
+ </div>
25
+
26
+ ### <strong>[Example Code](https://github.com/ScarysMonsters/spectre.db/tree/main/examples)</strong>
27
+
28
+ ---
29
+
30
+ ## Features
31
+
32
+ - [x] Zero external dependencies (pure Node.js built-ins)
33
+ - [x] WAL-based persistence — never rewrites the full file on every change
34
+ - [x] Atomic writes — crash-safe temp file + rename pattern
35
+ - [x] Backup rotation — configurable generations (`.1.bak`, `.2.bak`, ...)
36
+ - [x] O(1) LRU cache with prefix-index invalidation
37
+ - [x] Real transactions — function-based with automatic rollback on error
38
+ - [x] Legacy array transaction API — fully backward compatible
39
+ - [x] AES-256-GCM encryption — auto-applied to sensitive keys (`token`, `password`, `secret`, ...)
40
+ - [x] Table abstraction — scoped key namespacing
41
+ - [x] Dot-notation keys — `users.123.coins` works out of the box
42
+ - [x] Prototype pollution prevention by design
43
+ - [x] Events: `change`, `save`, `rollback`, `restore`, `clear`
44
+ - [ ] TypeScript types (planned)
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ > [!NOTE]
51
+ > **Node.js 18.0.0 or newer is required**
52
+
53
+ ```sh-session
54
+ npm install spectre.db@latest
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Quick Start
60
+
61
+ ```js
62
+ const { Database } = require('spectre.db');
63
+
64
+ const db = new Database('./data/mydb', {
65
+ cache: true,
66
+ autoSave: 5000,
67
+ backup: true,
68
+ });
69
+
70
+ db.ready.then(() => {
71
+ db.set('users.1.name', 'Alice');
72
+ console.log(db.get('users.1.name')); // 'Alice'
73
+ });
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Discord Bot Example
79
+
80
+ ```js
81
+ const { Client, GatewayIntentBits } = require('discord.js');
82
+ const { Database } = require('spectre.db');
83
+
84
+ const client = new Client({ intents: [GatewayIntentBits.Guilds] });
85
+
86
+ const db = new Database('./src/data/database', {
87
+ cache: true,
88
+ autoSave: 5000,
89
+ backup: true,
90
+ });
91
+
92
+ client.db = db;
93
+
94
+ db.ready.then(() => client.login(process.env.TOKEN));
95
+
96
+ client.once('ready', () => {
97
+ console.log(`${client.user.tag} is ready!`);
98
+ });
99
+
100
+ process.on('SIGINT', async () => {
101
+ await client.db.close();
102
+ process.exit(0);
103
+ });
104
+ ```
105
+
106
+ ---
107
+
108
+ ## API Reference
109
+
110
+ ### Constructor
111
+
112
+ ```js
113
+ new Database(path, options?)
114
+ ```
115
+
116
+ | Option | Type | Default | Description |
117
+ |---|---|---|---|
118
+ | `cache` | boolean | `true` | Enable LRU cache |
119
+ | `maxCacheSize` | number | `1000` | Max cached entries |
120
+ | `cacheTTL` | number | `0` | TTL in ms (0 = forever) |
121
+ | `autoSave` | number | `5000` | Compaction interval in ms |
122
+ | `backup` | boolean | `true` | Enable backup rotation |
123
+ | `backupCount` | number | `3` | Number of backup generations |
124
+ | `compress` | boolean | `false` | Gzip snapshot compression |
125
+ | `encryptionKey` | string/Buffer | `null` | AES-256-GCM key for sensitive values |
126
+ | `warmKeys` | string[] | `[]` | Keys to pre-load into cache on startup |
127
+
128
+ ---
129
+
130
+ ### Core Methods
131
+
132
+ ```js
133
+ db.get('users.1.name') // → value or null
134
+ db.set('users.1.name', 'Alice') // → value
135
+ db.delete('users.1.name') // → true / false
136
+ db.has('users.1.name') // → true / false
137
+ db.add('users.1.coins', 100) // → new value
138
+ db.sub('users.1.coins', 50) // → new value
139
+ db.push('users.1.roles', 'admin') // → new array length
140
+ db.pull('users.1.roles', 'member') // → true / false
141
+ ```
142
+
143
+ ### Query Methods
144
+
145
+ ```js
146
+ db.all() // → [{ ID, data }]
147
+ db.startsWith('users.') // → [{ ID, data }]
148
+ db.filter((data, id) => id.endsWith('.coins')) // → [{ ID, data }]
149
+ db.find((data, id) => id === 'users.1.name') // → { ID, data } | null
150
+ db.paginate('users.', page, limit, sortBy, sortDesc) // → { data, pagination }
151
+ ```
152
+
153
+ ### Transactions
154
+
155
+ ```js
156
+ // Function-based (recommended)
157
+ await db.transaction(async (tx) => {
158
+ const coins = tx.get('users.1.coins') ?? 0;
159
+ tx.set('users.1.coins', coins - 50);
160
+ tx.set('users.2.coins', (tx.get('users.2.coins') ?? 0) + 50);
161
+ });
162
+
163
+ // Array-based (legacy — fully supported)
164
+ await db.transaction([
165
+ { type: 'set', key: 'config.debug', value: true },
166
+ { type: 'add', key: 'stats.logins', value: 1 },
167
+ { type: 'delete', key: 'cache.tmp' },
168
+ ]);
169
+ ```
170
+
171
+ ### Tables
172
+
173
+ ```js
174
+ const users = db.table('users');
175
+
176
+ users.set('1.name', 'Alice') // stored as "users.1.name"
177
+ users.get('1.name') // 'Alice'
178
+ users.count() // number of entries
179
+ await users.clear() // removes all users.*
180
+
181
+ await users.transaction(async (tx) => {
182
+ tx.add('1.coins', 100);
183
+ });
184
+ ```
185
+
186
+ ### Persistence & Lifecycle
187
+
188
+ ```js
189
+ await db.save() // force compaction now
190
+ await db.close() // flush + compact + release (always call on shutdown)
191
+ db.getStats() // { entries, cacheSize, fileSize, walOps, ... }
192
+ ```
193
+
194
+ ---
195
+
196
+ ## How it works
197
+
198
+ ### Files on disk
199
+
200
+ ```
201
+ data/
202
+ ├── mydb.snapshot ← compacted JSON state
203
+ ├── mydb.wal ← append-only operation log
204
+ ├── mydb.snapshot.1.bak ← most recent backup
205
+ ├── mydb.snapshot.2.bak
206
+ └── mydb.snapshot.3.bak
207
+ ```
208
+
209
+ ### Write flow
210
+
211
+ ```
212
+ db.set('x', 1)
213
+ └─ updates in-memory store immediately (synchronous)
214
+ └─ WAL append queued (async, non-blocking)
215
+
216
+ Every compactInterval:
217
+ └─ if walOps >= compactThreshold:
218
+ └─ serialize → unique temp file → atomic rename → truncate WAL
219
+ ```
220
+
221
+ ### Startup flow
222
+
223
+ ```
224
+ new Database(path)
225
+ └─ load .snapshot
226
+ └─ replay .wal on top of snapshot
227
+ └─ open WAL for appending
228
+ └─ db.ready resolves
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Contributing
234
+
235
+ - Before creating an issue, please ensure that it hasn't already been reported/suggested.
236
+ - See [the contribution guide](https://github.com/ScarysMonsters/spectre.db/blob/main/.github/CONTRIBUTING.md) if you'd like to submit a PR.
237
+
238
+ ## Need help?
239
+
240
+ GitHub Issues: [Here](https://github.com/ScarysMonsters/spectre.db/issues)
241
+
242
+ ---
243
+
244
+ ## Other project(s)
245
+
246
+ - 🤖 [***ScarysMonsters***](https://github.com/ScarysMonsters) <br/>
247
+ More tools and bots.
248
+
249
+ ---
250
+
251
+ ## Star History
252
+
253
+ [![Star History Chart](https://api.star-history.com/svg?repos=ScarysMonsters/spectre.db&type=Date)](https://star-history.com/#ScarysMonsters/spectre.db&Date)
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const { Database } = require('spectre.db');
4
+
5
+ async function encryptionExample() {
6
+ console.log('--- Encryption ---');
7
+
8
+ const db = new Database('./data/secure', {
9
+ encryptionKey: 'my-super-secret-passphrase',
10
+ });
11
+
12
+ await db.ready;
13
+
14
+ db.set('user.token', 'discord-bot-token-abc123');
15
+ db.set('user.password', 'hunter2');
16
+ db.set('user.name', 'Alice');
17
+
18
+ console.log('token (decrypted via get):', db.get('user.token'));
19
+ console.log('name (plaintext):', db.get('user.name'));
20
+
21
+ await db.close();
22
+ console.log('');
23
+ }
24
+
25
+ async function tableExample() {
26
+ console.log('--- Tables ---');
27
+
28
+ const db = new Database('./data/tables');
29
+ await db.ready;
30
+
31
+ const guilds = db.table('guilds');
32
+ const users = db.table('users');
33
+
34
+ guilds.set('123.prefix', '!');
35
+ guilds.set('123.lang', 'en');
36
+ guilds.set('456.prefix', '?');
37
+
38
+ users.set('99.xp', 0);
39
+ users.set('99.level', 1);
40
+
41
+ await guilds.transaction(async (tx) => {
42
+ tx.set('123.prefix', '$');
43
+ tx.set('123.modRole', 'moderator');
44
+ });
45
+
46
+ console.log('guild prefix:', guilds.get('123.prefix'));
47
+ console.log('guild count:', guilds.count());
48
+ console.log('user xp:', users.get('99.xp'));
49
+
50
+ await db.close();
51
+ console.log('');
52
+ }
53
+
54
+ async function compactionExample() {
55
+ console.log('--- Compaction ---');
56
+
57
+ const db = new Database('./data/compact', {
58
+ compactThreshold: 5,
59
+ compactInterval: 1000,
60
+ });
61
+
62
+ await db.ready;
63
+
64
+ db.on('save', (stats) => {
65
+ console.log('compacted — walOps reset, fileSize:', stats.fileSize, 'bytes');
66
+ });
67
+
68
+ for (let i = 0; i < 10; i++) {
69
+ db.set(`counter.${i}`, i * 10);
70
+ }
71
+
72
+ await db.save();
73
+
74
+ console.log('stats after manual save:', db.getStats());
75
+ await db.close();
76
+ console.log('');
77
+ }
78
+
79
+ async function main() {
80
+ await encryptionExample();
81
+ await tableExample();
82
+ await compactionExample();
83
+ }
84
+
85
+ main().catch(console.error);
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ const { Database } = require('spectre.db');
4
+
5
+ async function main() {
6
+ const db = new Database('./data/mydb', {
7
+ cache: true,
8
+ autoSave: 5000,
9
+ backup: true,
10
+ });
11
+
12
+ await db.ready;
13
+
14
+ db.set('users.1.name', 'Alice');
15
+ db.set('users.1.coins', 0);
16
+ db.set('users.1.roles', ['member']);
17
+
18
+ console.log(db.get('users.1.name'));
19
+ console.log(db.has('users.1.coins'));
20
+
21
+ db.add('users.1.coins', 150);
22
+ db.sub('users.1.coins', 50);
23
+ console.log('coins:', db.get('users.1.coins'));
24
+
25
+ db.push('users.1.roles', 'admin');
26
+ console.log('roles:', db.get('users.1.roles'));
27
+
28
+ db.pull('users.1.roles', 'member');
29
+ console.log('roles after pull:', db.get('users.1.roles'));
30
+
31
+ db.set('users.2.name', 'Bob');
32
+ db.set('users.2.coins', 500);
33
+
34
+ const all = db.all();
35
+ console.log('all entries:', all.length);
36
+
37
+ const rich = db.filter((data, id) => id.endsWith('.coins') && data > 100);
38
+ console.log('rich users:', rich);
39
+
40
+ const { data, pagination } = db.paginate('users.', 1, 10, 'data', true);
41
+ console.log('page:', data.length, 'total:', pagination.total);
42
+
43
+ await db.transaction(async (tx) => {
44
+ tx.set('users.3.name', 'Charlie');
45
+ tx.set('users.3.coins', tx.get('users.1.coins') + 10);
46
+ });
47
+ console.log('tx result:', db.get('users.3.name'), db.get('users.3.coins'));
48
+
49
+ try {
50
+ await db.transaction(async (tx) => {
51
+ tx.set('users.4.name', 'Dave');
52
+ throw new Error('Something went wrong');
53
+ });
54
+ } catch {
55
+ console.log('rollback ok — users.4 exists:', db.has('users.4.name'));
56
+ }
57
+
58
+ const users = db.table('users');
59
+ console.log('table count:', users.count());
60
+ console.log('table get 1.name:', users.get('1.name'));
61
+
62
+ await db.transaction([
63
+ { type: 'set', key: 'config.debug', value: true },
64
+ { type: 'set', key: 'config.version', value: 2 },
65
+ { type: 'delete', key: 'users.3.name' },
66
+ ]);
67
+ console.log('legacy tx config.debug:', db.get('config.debug'));
68
+ console.log('legacy tx users.3.name (deleted):', db.get('users.3.name'));
69
+
70
+ console.log('stats:', db.getStats());
71
+
72
+ await db.close();
73
+ console.log('closed.');
74
+ }
75
+
76
+ main().catch(console.error);
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const { Client, GatewayIntentBits } = require('discord.js');
4
+ const { Database } = require('spectre.db');
5
+
6
+ const client = new Client({
7
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent],
8
+ });
9
+
10
+ const db = new Database('./src/data/database', {
11
+ cache: true,
12
+ autoSave: 5000,
13
+ backup: true,
14
+ });
15
+
16
+ client.db = db;
17
+
18
+ db.ready.then(() => {
19
+ console.log('[DB] Ready');
20
+ client.login(process.env.TOKEN);
21
+ });
22
+
23
+ client.once('ready', () => {
24
+ console.log(`[Bot] Logged in as ${client.user.tag}`);
25
+ });
26
+
27
+ client.on('messageCreate', async (message) => {
28
+ if (message.author.bot) return;
29
+
30
+ const prefix = '!';
31
+ if (!message.content.startsWith(prefix)) return;
32
+
33
+ const args = message.content.slice(prefix.length).trim().split(/\s+/);
34
+ const command = args.shift().toLowerCase();
35
+
36
+ if (command === 'coins') {
37
+ const userId = message.author.id;
38
+ const coins = client.db.get(`users.${userId}.coins`) ?? 0;
39
+ return message.reply(`You have **${coins}** coins.`);
40
+ }
41
+
42
+ if (command === 'daily') {
43
+ const userId = message.author.id;
44
+ const lastKey = `users.${userId}.lastDaily`;
45
+ const last = client.db.get(lastKey) ?? 0;
46
+ const now = Date.now();
47
+ const cooldown = 24 * 60 * 60 * 1000;
48
+
49
+ if (now - last < cooldown) {
50
+ const remaining = Math.ceil((cooldown - (now - last)) / 3600000);
51
+ return message.reply(`Come back in ${remaining}h for your daily coins.`);
52
+ }
53
+
54
+ await client.db.transaction(async (tx) => {
55
+ const current = tx.get(`users.${userId}.coins`) ?? 0;
56
+ tx.set(`users.${userId}.coins`, current + 100);
57
+ tx.set(`users.${userId}.lastDaily`, now);
58
+ });
59
+
60
+ return message.reply('You claimed your **100** daily coins!');
61
+ }
62
+
63
+ if (command === 'give') {
64
+ const target = message.mentions.users.first();
65
+ const amount = parseInt(args[1], 10);
66
+
67
+ if (!target || isNaN(amount) || amount <= 0) {
68
+ return message.reply('Usage: `!give @user <amount>`');
69
+ }
70
+
71
+ const senderId = message.author.id;
72
+ const balance = client.db.get(`users.${senderId}.coins`) ?? 0;
73
+
74
+ if (balance < amount) {
75
+ return message.reply(`You only have **${balance}** coins.`);
76
+ }
77
+
78
+ await client.db.transaction(async (tx) => {
79
+ const senderCoins = tx.get(`users.${senderId}.coins`) ?? 0;
80
+ const receiverCoins = tx.get(`users.${target.id}.coins`) ?? 0;
81
+ tx.set(`users.${senderId}.coins`, senderCoins - amount);
82
+ tx.set(`users.${target.id}.coins`, receiverCoins + amount);
83
+ });
84
+
85
+ return message.reply(`Sent **${amount}** coins to ${target.username}.`);
86
+ }
87
+
88
+ if (command === 'leaderboard') {
89
+ const { data } = client.db.paginate('users.', 1, 10, 'data', true);
90
+ const filtered = data.filter(({ ID }) => ID.endsWith('.coins'));
91
+ const lines = filtered.map(({ ID, data: coins }, i) => {
92
+ const uid = ID.split('.')[1];
93
+ return `${i + 1}. <@${uid}> — ${coins} coins`;
94
+ });
95
+ return message.reply(lines.length ? lines.join('\n') : 'No data yet.');
96
+ }
97
+ });
98
+
99
+ process.on('SIGINT', async () => {
100
+ console.log('[Bot] Shutting down...');
101
+ await client.db.close();
102
+ process.exit(0);
103
+ });