ziplayer 0.2.7-dev.1 → 0.2.7-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/dist/persistence/PersistenceManager.d.ts +50 -16
- package/dist/persistence/PersistenceManager.d.ts.map +1 -1
- package/dist/persistence/PersistenceManager.js +476 -59
- package/dist/persistence/PersistenceManager.js.map +1 -1
- package/dist/structures/Player.d.ts +1 -0
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +4 -0
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +3 -1
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +49 -31
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/persistence.d.ts +24 -5
- package/dist/types/persistence.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/persistence/PersistenceManager.ts +563 -62
- package/src/structures/Player.ts +6 -0
- package/src/structures/PlayerManager.ts +66 -32
- package/src/types/index.ts +9 -0
- package/src/types/persistence.ts +35 -17
|
@@ -3,7 +3,15 @@ import * as fs from "fs";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as zlib from "zlib";
|
|
5
5
|
import { promisify } from "util";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
SerializedPlayer,
|
|
8
|
+
SerializedQueue,
|
|
9
|
+
SerializedTrack,
|
|
10
|
+
PersistenceOptions,
|
|
11
|
+
PersistenceProvider,
|
|
12
|
+
DestroyedRecord,
|
|
13
|
+
BackupInfo,
|
|
14
|
+
} from "../types";
|
|
7
15
|
import type { Player } from "../structures/Player";
|
|
8
16
|
import type { PlayerManager } from "../structures/PlayerManager";
|
|
9
17
|
import type { Track } from "../types";
|
|
@@ -11,14 +19,26 @@ import type { Track } from "../types";
|
|
|
11
19
|
const gzip = promisify(zlib.gzip);
|
|
12
20
|
const gunzip = promisify(zlib.gunzip);
|
|
13
21
|
|
|
14
|
-
// File provider implementation
|
|
15
|
-
|
|
22
|
+
// File provider implementation with enhanced backup management
|
|
23
|
+
class FileProvider implements PersistenceProvider {
|
|
16
24
|
private basePath: string;
|
|
17
25
|
private maxBackups: number;
|
|
26
|
+
private maxTotalBackups: number;
|
|
27
|
+
private backupRetentionDays: number;
|
|
18
28
|
|
|
19
|
-
constructor(
|
|
29
|
+
constructor(
|
|
30
|
+
basePath: string,
|
|
31
|
+
options: {
|
|
32
|
+
maxBackups?: number;
|
|
33
|
+
maxTotalBackups?: number;
|
|
34
|
+
backupRetentionDays?: number;
|
|
35
|
+
} = {},
|
|
36
|
+
) {
|
|
20
37
|
this.basePath = basePath;
|
|
21
|
-
this.maxBackups = maxBackups;
|
|
38
|
+
this.maxBackups = options.maxBackups ?? 5;
|
|
39
|
+
this.maxTotalBackups = options.maxTotalBackups ?? 50;
|
|
40
|
+
this.backupRetentionDays = options.backupRetentionDays ?? 7;
|
|
41
|
+
|
|
22
42
|
if (!fs.existsSync(basePath)) {
|
|
23
43
|
fs.mkdirSync(basePath, { recursive: true });
|
|
24
44
|
}
|
|
@@ -32,18 +52,165 @@ export class FileProvider implements PersistenceProvider {
|
|
|
32
52
|
return path.join(this.basePath, `${key}_backup_${timestamp}.json`);
|
|
33
53
|
}
|
|
34
54
|
|
|
55
|
+
private getAllBackups(): BackupInfo[] {
|
|
56
|
+
const files = fs.readdirSync(this.basePath);
|
|
57
|
+
const backups: BackupInfo[] = [];
|
|
58
|
+
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const match = file.match(/^(.+)_backup_(\d+)\.json(\.gz)?$/);
|
|
61
|
+
if (match) {
|
|
62
|
+
const [, key, timestampStr] = match;
|
|
63
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
64
|
+
const filePath = path.join(this.basePath, file);
|
|
65
|
+
const stats = fs.statSync(filePath);
|
|
66
|
+
const isCompressed = file.endsWith(".gz");
|
|
67
|
+
|
|
68
|
+
backups.push({
|
|
69
|
+
key,
|
|
70
|
+
path: filePath,
|
|
71
|
+
timestamp,
|
|
72
|
+
size: stats.size,
|
|
73
|
+
isCompressed,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return backups.sort((a, b) => b.timestamp - a.timestamp);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private getBackupsByKey(key: string): BackupInfo[] {
|
|
82
|
+
return this.getAllBackups().filter((b) => b.key === key);
|
|
83
|
+
}
|
|
84
|
+
|
|
35
85
|
private cleanOldBackups(key: string): void {
|
|
36
|
-
const backups =
|
|
37
|
-
.readdirSync(this.basePath)
|
|
38
|
-
.filter((f) => f.startsWith(key) && f.includes("backup"))
|
|
39
|
-
.sort()
|
|
40
|
-
.reverse();
|
|
86
|
+
const backups = this.getBackupsByKey(key);
|
|
41
87
|
|
|
42
|
-
//
|
|
88
|
+
// Delete old backups exceeding maxBackups per player
|
|
43
89
|
for (let i = this.maxBackups; i < backups.length; i++) {
|
|
44
|
-
|
|
45
|
-
|
|
90
|
+
try {
|
|
91
|
+
fs.unlinkSync(backups[i].path);
|
|
92
|
+
console.log(`[Persistence] Deleted old backup: ${path.basename(backups[i].path)}`);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`[Persistence] Failed to delete backup: ${backups[i].path}`, err);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private cleanOldBackupsByAge(): void {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const retentionMs = this.backupRetentionDays * 24 * 60 * 60 * 1000;
|
|
102
|
+
const backups = this.getAllBackups();
|
|
103
|
+
|
|
104
|
+
let deletedCount = 0;
|
|
105
|
+
for (const backup of backups) {
|
|
106
|
+
if (now - backup.timestamp > retentionMs) {
|
|
107
|
+
try {
|
|
108
|
+
fs.unlinkSync(backup.path);
|
|
109
|
+
deletedCount++;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(`[Persistence] Failed to delete old backup: ${backup.path}`, err);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (deletedCount > 0) {
|
|
117
|
+
console.log(`[Persistence] Deleted ${deletedCount} backups older than ${this.backupRetentionDays} days`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private cleanTotalBackupsLimit(): void {
|
|
122
|
+
let backups = this.getAllBackups();
|
|
123
|
+
|
|
124
|
+
if (backups.length <= this.maxTotalBackups) return;
|
|
125
|
+
|
|
126
|
+
// Delete oldest backups
|
|
127
|
+
const toDelete = backups.slice(this.maxTotalBackups);
|
|
128
|
+
let deletedCount = 0;
|
|
129
|
+
|
|
130
|
+
for (const backup of toDelete) {
|
|
131
|
+
try {
|
|
132
|
+
fs.unlinkSync(backup.path);
|
|
133
|
+
deletedCount++;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (deletedCount > 0) {
|
|
140
|
+
console.log(`[Persistence] Deleted ${deletedCount} backups (exceeded limit ${this.maxTotalBackups})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// NEW: Clean all backups for a specific player
|
|
145
|
+
async cleanAllBackupsForPlayer(key: string): Promise<number> {
|
|
146
|
+
const backups = this.getBackupsByKey(key);
|
|
147
|
+
let deletedCount = 0;
|
|
148
|
+
|
|
149
|
+
for (const backup of backups) {
|
|
150
|
+
try {
|
|
151
|
+
fs.unlinkSync(backup.path);
|
|
152
|
+
deletedCount++;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (deletedCount > 0) {
|
|
159
|
+
console.log(`[Persistence] Deleted ${deletedCount} backups for player: ${key}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return deletedCount;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// NEW: Clean all backups
|
|
166
|
+
async cleanAllBackups(): Promise<number> {
|
|
167
|
+
const backups = this.getAllBackups();
|
|
168
|
+
let deletedCount = 0;
|
|
169
|
+
|
|
170
|
+
for (const backup of backups) {
|
|
171
|
+
try {
|
|
172
|
+
fs.unlinkSync(backup.path);
|
|
173
|
+
deletedCount++;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (deletedCount > 0) {
|
|
180
|
+
console.log(`[Persistence] Deleted all ${deletedCount} backups`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return deletedCount;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// NEW: Get backup statistics
|
|
187
|
+
getBackupStats(): {
|
|
188
|
+
totalBackups: number;
|
|
189
|
+
totalSize: number;
|
|
190
|
+
oldestBackup: number | null;
|
|
191
|
+
newestBackup: number | null;
|
|
192
|
+
backupsByPlayer: Map<string, number>;
|
|
193
|
+
} {
|
|
194
|
+
const backups = this.getAllBackups();
|
|
195
|
+
let totalSize = 0;
|
|
196
|
+
let oldestBackup: number | null = null;
|
|
197
|
+
let newestBackup: number | null = null;
|
|
198
|
+
const backupsByPlayer = new Map<string, number>();
|
|
199
|
+
|
|
200
|
+
for (const backup of backups) {
|
|
201
|
+
totalSize += backup.size;
|
|
202
|
+
if (oldestBackup === null || backup.timestamp < oldestBackup) oldestBackup = backup.timestamp;
|
|
203
|
+
if (newestBackup === null || backup.timestamp > newestBackup) newestBackup = backup.timestamp;
|
|
204
|
+
backupsByPlayer.set(backup.key, (backupsByPlayer.get(backup.key) || 0) + 1);
|
|
46
205
|
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
totalBackups: backups.length,
|
|
209
|
+
totalSize,
|
|
210
|
+
oldestBackup,
|
|
211
|
+
newestBackup,
|
|
212
|
+
backupsByPlayer,
|
|
213
|
+
};
|
|
47
214
|
}
|
|
48
215
|
|
|
49
216
|
async save(key: string, data: any, compress: boolean = false): Promise<void> {
|
|
@@ -62,6 +229,8 @@ export class FileProvider implements PersistenceProvider {
|
|
|
62
229
|
const backupPath = this.getBackupPath(key, Date.now());
|
|
63
230
|
fs.copyFileSync(filePath, backupPath);
|
|
64
231
|
this.cleanOldBackups(key);
|
|
232
|
+
this.cleanTotalBackupsLimit();
|
|
233
|
+
this.cleanOldBackupsByAge();
|
|
65
234
|
}
|
|
66
235
|
|
|
67
236
|
fs.writeFileSync(filePath, content);
|
|
@@ -92,11 +261,20 @@ export class FileProvider implements PersistenceProvider {
|
|
|
92
261
|
|
|
93
262
|
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
94
263
|
if (fs.existsSync(gzPath)) fs.unlinkSync(gzPath);
|
|
264
|
+
|
|
265
|
+
// Also delete backups for this player
|
|
266
|
+
await this.cleanAllBackupsForPlayer(key);
|
|
95
267
|
}
|
|
96
268
|
|
|
97
269
|
async list(): Promise<string[]> {
|
|
98
270
|
const files = fs.readdirSync(this.basePath);
|
|
99
|
-
return files
|
|
271
|
+
return files
|
|
272
|
+
.filter((f) => {
|
|
273
|
+
// Exclude backup files
|
|
274
|
+
if (f.includes("_backup_")) return false;
|
|
275
|
+
return f.endsWith(".json") || f.endsWith(".json.gz");
|
|
276
|
+
})
|
|
277
|
+
.map((f) => f.replace(/\.json(\.gz)?$/, ""));
|
|
100
278
|
}
|
|
101
279
|
|
|
102
280
|
async restoreBackup(key: string, backupTimestamp?: number): Promise<boolean> {
|
|
@@ -109,13 +287,9 @@ export class FileProvider implements PersistenceProvider {
|
|
|
109
287
|
}
|
|
110
288
|
} else {
|
|
111
289
|
// Get latest backup
|
|
112
|
-
const backups =
|
|
113
|
-
.readdirSync(this.basePath)
|
|
114
|
-
.filter((f) => f.startsWith(key) && f.includes("backup"))
|
|
115
|
-
.sort()
|
|
116
|
-
.reverse();
|
|
290
|
+
const backups = this.getBackupsByKey(key);
|
|
117
291
|
if (backups.length > 0) {
|
|
118
|
-
backupFile =
|
|
292
|
+
backupFile = backups[0].path;
|
|
119
293
|
}
|
|
120
294
|
}
|
|
121
295
|
|
|
@@ -130,7 +304,7 @@ export class FileProvider implements PersistenceProvider {
|
|
|
130
304
|
}
|
|
131
305
|
}
|
|
132
306
|
|
|
133
|
-
// Custom provider for database integration
|
|
307
|
+
// Custom provider for database integration (giữ nguyên)
|
|
134
308
|
class CustomProvider implements PersistenceProvider {
|
|
135
309
|
constructor(
|
|
136
310
|
private saveFn: (key: string, data: any) => Promise<void>,
|
|
@@ -164,30 +338,46 @@ class CustomProvider implements PersistenceProvider {
|
|
|
164
338
|
export class PersistenceManager extends EventEmitter {
|
|
165
339
|
private manager: PlayerManager;
|
|
166
340
|
private options: PersistenceOptions;
|
|
167
|
-
private provider:
|
|
341
|
+
private provider: FileProvider | CustomProvider;
|
|
168
342
|
private saveInterval: NodeJS.Timeout | null = null;
|
|
169
343
|
private isSaving: boolean = false;
|
|
344
|
+
private isRestoring: boolean = false;
|
|
345
|
+
private destroyedPlayers: Map<string, DestroyedRecord> = new Map();
|
|
346
|
+
private restoredPlayers: Set<string> = new Set();
|
|
347
|
+
private backupCleanupDone: boolean = false;
|
|
170
348
|
|
|
171
349
|
constructor(manager: PlayerManager, options: PersistenceOptions) {
|
|
172
350
|
super();
|
|
173
351
|
this.manager = manager;
|
|
174
|
-
|
|
352
|
+
|
|
353
|
+
// Default options
|
|
175
354
|
this.options = {
|
|
176
355
|
enabled: true,
|
|
177
356
|
provider: "file",
|
|
178
357
|
saveInterval: 60000,
|
|
179
358
|
autoLoad: true,
|
|
359
|
+
autoRestoreOnRestart: true,
|
|
360
|
+
restoreDelay: 5000,
|
|
180
361
|
maxBackups: 5,
|
|
362
|
+
maxTotalBackups: 10,
|
|
363
|
+
autoCleanupBackupsOnStart: true,
|
|
364
|
+
backupRetentionDays: 2,
|
|
181
365
|
compress: false,
|
|
182
366
|
filePath: "./players_data",
|
|
183
367
|
};
|
|
184
368
|
|
|
185
|
-
// Merge
|
|
369
|
+
// Merge manually
|
|
186
370
|
if (options.enabled !== undefined) this.options.enabled = options.enabled;
|
|
187
371
|
if (options.provider !== undefined) this.options.provider = options.provider;
|
|
188
372
|
if (options.saveInterval !== undefined) this.options.saveInterval = options.saveInterval;
|
|
189
373
|
if (options.autoLoad !== undefined) this.options.autoLoad = options.autoLoad;
|
|
374
|
+
if (options.autoRestoreOnRestart !== undefined) this.options.autoRestoreOnRestart = options.autoRestoreOnRestart;
|
|
375
|
+
if (options.restoreDelay !== undefined) this.options.restoreDelay = options.restoreDelay;
|
|
190
376
|
if (options.maxBackups !== undefined) this.options.maxBackups = options.maxBackups;
|
|
377
|
+
if (options.maxTotalBackups !== undefined) this.options.maxTotalBackups = options.maxTotalBackups;
|
|
378
|
+
if (options.autoCleanupBackupsOnStart !== undefined)
|
|
379
|
+
this.options.autoCleanupBackupsOnStart = options.autoCleanupBackupsOnStart;
|
|
380
|
+
if (options.backupRetentionDays !== undefined) this.options.backupRetentionDays = options.backupRetentionDays;
|
|
191
381
|
if (options.compress !== undefined) this.options.compress = options.compress;
|
|
192
382
|
if (options.filePath !== undefined) this.options.filePath = options.filePath;
|
|
193
383
|
if (options.redisUrl !== undefined) this.options.redisUrl = options.redisUrl;
|
|
@@ -199,6 +389,16 @@ export class PersistenceManager extends EventEmitter {
|
|
|
199
389
|
|
|
200
390
|
this.provider = this.createProvider();
|
|
201
391
|
|
|
392
|
+
// Hook into player destroy events
|
|
393
|
+
this.setupDestroyTracking();
|
|
394
|
+
|
|
395
|
+
// Clean up old backups on start
|
|
396
|
+
if (this.options.enabled && this.options.autoCleanupBackupsOnStart) {
|
|
397
|
+
this.cleanupBackupsOnStart().catch((err) => {
|
|
398
|
+
this.debug("Backup cleanup on start error:", err);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
202
402
|
if (this.options.enabled) {
|
|
203
403
|
this.startAutoSave();
|
|
204
404
|
|
|
@@ -210,28 +410,33 @@ export class PersistenceManager extends EventEmitter {
|
|
|
210
410
|
}
|
|
211
411
|
}
|
|
212
412
|
|
|
213
|
-
private
|
|
413
|
+
private setupDestroyTracking(): void {
|
|
414
|
+
this.manager.on("playerDestroy", (player: Player) => {
|
|
415
|
+
this.markAsDestroyed(player.guildId);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private createProvider(): FileProvider | CustomProvider {
|
|
214
420
|
switch (this.options.provider) {
|
|
215
421
|
case "file":
|
|
216
|
-
return new FileProvider(this.options.filePath!,
|
|
422
|
+
return new FileProvider(this.options.filePath!, {
|
|
423
|
+
maxBackups: this.options.maxBackups,
|
|
424
|
+
maxTotalBackups: this.options.maxTotalBackups,
|
|
425
|
+
backupRetentionDays: this.options.backupRetentionDays,
|
|
426
|
+
});
|
|
217
427
|
case "redis":
|
|
218
|
-
// Implement Redis provider if needed
|
|
219
428
|
throw new Error("Redis provider not implemented yet");
|
|
220
429
|
case "database":
|
|
221
430
|
if (!this.options.save || !this.options.load) {
|
|
222
431
|
throw new Error("Database provider requires save/load functions");
|
|
223
432
|
}
|
|
224
|
-
// Fix: Pass the save and load functions with correct signatures
|
|
225
433
|
return new CustomProvider(
|
|
226
434
|
async (key: string, data: any) => {
|
|
227
435
|
if (this.options.save) {
|
|
228
|
-
// Call with single object argument if that's expected
|
|
229
436
|
const saveFn = this.options.save as any;
|
|
230
437
|
if (saveFn.length === 1) {
|
|
231
|
-
// Save function expects { key, data }
|
|
232
438
|
await saveFn({ key, data });
|
|
233
439
|
} else {
|
|
234
|
-
// Save function expects (key, data)
|
|
235
440
|
await saveFn(key, data);
|
|
236
441
|
}
|
|
237
442
|
}
|
|
@@ -240,11 +445,9 @@ export class PersistenceManager extends EventEmitter {
|
|
|
240
445
|
if (this.options.load) {
|
|
241
446
|
const loadFn = this.options.load as any;
|
|
242
447
|
if (loadFn.length === 0) {
|
|
243
|
-
// Load function expects no args, returns all data
|
|
244
448
|
const allData = await loadFn();
|
|
245
449
|
return allData?.get?.(key) || allData?.[key] || null;
|
|
246
450
|
} else {
|
|
247
|
-
// Load function expects key
|
|
248
451
|
return await loadFn(key);
|
|
249
452
|
}
|
|
250
453
|
}
|
|
@@ -254,7 +457,11 @@ export class PersistenceManager extends EventEmitter {
|
|
|
254
457
|
this.options.list,
|
|
255
458
|
);
|
|
256
459
|
default:
|
|
257
|
-
return new FileProvider(this.options.filePath!,
|
|
460
|
+
return new FileProvider(this.options.filePath!, {
|
|
461
|
+
maxBackups: this.options.maxBackups,
|
|
462
|
+
maxTotalBackups: this.options.maxTotalBackups,
|
|
463
|
+
backupRetentionDays: this.options.backupRetentionDays,
|
|
464
|
+
});
|
|
258
465
|
}
|
|
259
466
|
}
|
|
260
467
|
|
|
@@ -279,8 +486,154 @@ export class PersistenceManager extends EventEmitter {
|
|
|
279
486
|
this.debug(`Auto-save started (interval: ${this.options.saveInterval}ms)`);
|
|
280
487
|
}
|
|
281
488
|
|
|
489
|
+
// NEW: Cleanup backups on startup
|
|
490
|
+
private async cleanupBackupsOnStart(): Promise<void> {
|
|
491
|
+
if (this.backupCleanupDone) return;
|
|
492
|
+
|
|
493
|
+
this.debug("Starting backup cleanup on startup...");
|
|
494
|
+
|
|
495
|
+
// Only works for file provider
|
|
496
|
+
if (this.provider instanceof FileProvider) {
|
|
497
|
+
try {
|
|
498
|
+
// Clean old backups by age
|
|
499
|
+
// This is already handled in FileProvider, but we can log stats
|
|
500
|
+
const stats = this.provider.getBackupStats();
|
|
501
|
+
|
|
502
|
+
this.debug(`Backup stats before cleanup:`, {
|
|
503
|
+
totalBackups: stats.totalBackups,
|
|
504
|
+
totalSizeMB: (stats.totalSize / 1024 / 1024).toFixed(2),
|
|
505
|
+
oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup).toISOString() : null,
|
|
506
|
+
newestBackup: stats.newestBackup ? new Date(stats.newestBackup).toISOString() : null,
|
|
507
|
+
backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Emit stats event
|
|
511
|
+
this.emit("backupStats", stats);
|
|
512
|
+
|
|
513
|
+
// The cleanup is already happening in FileProvider.save()
|
|
514
|
+
// But we can do a one-time deep cleanup on start
|
|
515
|
+
if (this.options.backupRetentionDays && this.options.backupRetentionDays > 0) {
|
|
516
|
+
// Force a cleanup pass
|
|
517
|
+
const deletedCount = await this.cleanOldBackupsByAge();
|
|
518
|
+
if (deletedCount > 0) {
|
|
519
|
+
this.debug(`Cleaned up ${deletedCount} old backups on startup`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Enforce total backup limit
|
|
524
|
+
const totalLimitDeleted = await this.enforceTotalBackupLimit();
|
|
525
|
+
if (totalLimitDeleted > 0) {
|
|
526
|
+
this.debug(`Enforced backup limit: deleted ${totalLimitDeleted} backups`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
this.backupCleanupDone = true;
|
|
530
|
+
this.emit("backupCleanupDone");
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.debug("Backup cleanup error:", error);
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
this.debug("Backup cleanup only supported for file provider");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// NEW: Clean old backups by age
|
|
540
|
+
private async cleanOldBackupsByAge(): Promise<number> {
|
|
541
|
+
if (!(this.provider instanceof FileProvider)) return 0;
|
|
542
|
+
|
|
543
|
+
const retentionMs = (this.options.backupRetentionDays ?? 2) * 24 * 60 * 60 * 1000;
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
const backups = (this.provider as any).getAllBackups();
|
|
546
|
+
let deletedCount = 0;
|
|
547
|
+
|
|
548
|
+
for (const backup of backups) {
|
|
549
|
+
if (now - backup.timestamp > retentionMs) {
|
|
550
|
+
try {
|
|
551
|
+
fs.unlinkSync(backup.path);
|
|
552
|
+
deletedCount++;
|
|
553
|
+
} catch (err) {
|
|
554
|
+
this.debug(`Failed to delete old backup: ${backup.path}`, err);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (deletedCount > 0) {
|
|
560
|
+
this.debug(`Deleted ${deletedCount} backups older than ${this.options.backupRetentionDays} days`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return deletedCount;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// NEW: Enforce total backup limit
|
|
567
|
+
private async enforceTotalBackupLimit(): Promise<number> {
|
|
568
|
+
if (!(this.provider instanceof FileProvider)) return 0;
|
|
569
|
+
|
|
570
|
+
const backups = (this.provider as any).getAllBackups();
|
|
571
|
+
const maxTotal = this.options.maxTotalBackups ?? 50;
|
|
572
|
+
|
|
573
|
+
if (backups.length <= maxTotal) return 0;
|
|
574
|
+
|
|
575
|
+
const toDelete = backups.slice(maxTotal);
|
|
576
|
+
let deletedCount = 0;
|
|
577
|
+
|
|
578
|
+
for (const backup of toDelete) {
|
|
579
|
+
try {
|
|
580
|
+
fs.unlinkSync(backup.path);
|
|
581
|
+
deletedCount++;
|
|
582
|
+
} catch (err) {
|
|
583
|
+
this.debug(`Failed to delete backup: ${backup.path}`, err);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (deletedCount > 0) {
|
|
588
|
+
this.debug(`Deleted ${deletedCount} backups (exceeded limit ${maxTotal})`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return deletedCount;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private markAsDestroyed(guildId: string): void {
|
|
595
|
+
this.destroyedPlayers.set(guildId, {
|
|
596
|
+
guildId,
|
|
597
|
+
destroyedAt: Date.now(),
|
|
598
|
+
reason: "player_destroy",
|
|
599
|
+
});
|
|
600
|
+
this.debug(`Marked player as destroyed: ${guildId}`);
|
|
601
|
+
|
|
602
|
+
this.saveDestroyedStatus().catch((err) => {
|
|
603
|
+
this.debug("Failed to save destroyed status:", err);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private isDestroyed(guildId: string): boolean {
|
|
608
|
+
return this.destroyedPlayers.has(guildId);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private async saveDestroyedStatus(): Promise<void> {
|
|
612
|
+
const destroyedData = Array.from(this.destroyedPlayers.values());
|
|
613
|
+
await this.provider.save("__destroyed_players__", destroyedData);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private async loadDestroyedStatus(): Promise<void> {
|
|
617
|
+
try {
|
|
618
|
+
const data = await this.provider.load("__destroyed_players__");
|
|
619
|
+
if (data && Array.isArray(data)) {
|
|
620
|
+
this.destroyedPlayers.clear();
|
|
621
|
+
for (const record of data) {
|
|
622
|
+
this.destroyedPlayers.set(record.guildId, record);
|
|
623
|
+
}
|
|
624
|
+
this.debug(`Loaded ${this.destroyedPlayers.size} destroyed player records`);
|
|
625
|
+
}
|
|
626
|
+
} catch (error) {
|
|
627
|
+
this.debug("Failed to load destroyed status:", error);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private async clearDestroyedStatus(guildId: string): Promise<void> {
|
|
632
|
+
this.destroyedPlayers.delete(guildId);
|
|
633
|
+
await this.saveDestroyedStatus();
|
|
634
|
+
}
|
|
635
|
+
|
|
282
636
|
private serializeTrack(track: Track): SerializedTrack {
|
|
283
|
-
// Create base object with required fields only (avoid duplication)
|
|
284
637
|
const serialized: SerializedTrack = {
|
|
285
638
|
id: track.id,
|
|
286
639
|
title: track.title,
|
|
@@ -292,12 +645,10 @@ export class PersistenceManager extends EventEmitter {
|
|
|
292
645
|
isLive: track.isLive || false,
|
|
293
646
|
};
|
|
294
647
|
|
|
295
|
-
// Add optional fields if they exist on the track
|
|
296
648
|
const trackAny = track as any;
|
|
297
649
|
if (trackAny.author) serialized.author = trackAny.author;
|
|
298
650
|
if (trackAny.artwork) serialized.artwork = trackAny.artwork;
|
|
299
651
|
|
|
300
|
-
// Add any extra metadata (excluding fields we already set)
|
|
301
652
|
const excludedFields = new Set([
|
|
302
653
|
"id",
|
|
303
654
|
"title",
|
|
@@ -331,16 +682,13 @@ export class PersistenceManager extends EventEmitter {
|
|
|
331
682
|
}
|
|
332
683
|
|
|
333
684
|
private serializePlayer(player: Player): SerializedPlayer {
|
|
334
|
-
// Get filters safely - access through public method
|
|
335
685
|
let filters: string[] = [];
|
|
336
686
|
try {
|
|
337
687
|
const filterString = (player as any).filter?.getFilterString();
|
|
338
688
|
if (filterString) {
|
|
339
689
|
filters = filterString.split(",").filter(Boolean);
|
|
340
690
|
}
|
|
341
|
-
} catch (e) {
|
|
342
|
-
// Filter may not be accessible
|
|
343
|
-
}
|
|
691
|
+
} catch (e) {}
|
|
344
692
|
|
|
345
693
|
return {
|
|
346
694
|
guildId: player.guildId,
|
|
@@ -352,11 +700,11 @@ export class PersistenceManager extends EventEmitter {
|
|
|
352
700
|
filters: filters.length > 0 ? filters : undefined,
|
|
353
701
|
lastUpdate: Date.now(),
|
|
354
702
|
version: "1.0.0",
|
|
703
|
+
wasDestroyed: false,
|
|
355
704
|
};
|
|
356
705
|
}
|
|
357
706
|
|
|
358
707
|
private deserializeTrack(data: SerializedTrack): Track {
|
|
359
|
-
// Create base track object
|
|
360
708
|
const track: any = {
|
|
361
709
|
id: data.id,
|
|
362
710
|
title: data.title,
|
|
@@ -368,11 +716,9 @@ export class PersistenceManager extends EventEmitter {
|
|
|
368
716
|
isLive: data.isLive || false,
|
|
369
717
|
};
|
|
370
718
|
|
|
371
|
-
// Add optional fields if they exist
|
|
372
719
|
if (data.author) track.author = data.author;
|
|
373
720
|
if (data.artwork) track.artwork = data.artwork;
|
|
374
721
|
|
|
375
|
-
// Add any extra metadata from serialized data
|
|
376
722
|
for (const key of Object.keys(data)) {
|
|
377
723
|
if (
|
|
378
724
|
!["id", "title", "url", "source", "duration", "thumbnail", "requestedBy", "isLive", "author", "artwork"].includes(key)
|
|
@@ -390,9 +736,14 @@ export class PersistenceManager extends EventEmitter {
|
|
|
390
736
|
async savePlayer(player: Player): Promise<boolean> {
|
|
391
737
|
if (!this.options.enabled) return false;
|
|
392
738
|
|
|
739
|
+
if (this.isDestroyed(player.guildId)) {
|
|
740
|
+
this.debug(`Skipping save for destroyed player: ${player.guildId}`);
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
|
|
393
744
|
try {
|
|
394
745
|
const data = this.serializePlayer(player);
|
|
395
|
-
await this.provider.save(player.guildId, data);
|
|
746
|
+
await this.provider.save(player.guildId, data, this.options.compress);
|
|
396
747
|
this.debug(`Saved player: ${player.guildId}`);
|
|
397
748
|
this.emit("playerSaved", player.guildId);
|
|
398
749
|
return true;
|
|
@@ -416,7 +767,6 @@ export class PersistenceManager extends EventEmitter {
|
|
|
416
767
|
const players = this.manager.getAll();
|
|
417
768
|
this.debug(`Saving ${players.length} players...`);
|
|
418
769
|
|
|
419
|
-
// Save in parallel with limit
|
|
420
770
|
const batchSize = 5;
|
|
421
771
|
for (let i = 0; i < players.length; i += batchSize) {
|
|
422
772
|
const batch = players.slice(i, i + batchSize);
|
|
@@ -437,39 +787,49 @@ export class PersistenceManager extends EventEmitter {
|
|
|
437
787
|
/**
|
|
438
788
|
* Load a single player
|
|
439
789
|
*/
|
|
440
|
-
async loadPlayer(guildId: string, restorePosition: boolean = true): Promise<boolean> {
|
|
790
|
+
async loadPlayer(guildId: string, restorePosition: boolean = true, skipIfDestroyed: boolean = true): Promise<boolean> {
|
|
441
791
|
if (!this.options.enabled) return false;
|
|
442
792
|
|
|
793
|
+
if (skipIfDestroyed && this.isDestroyed(guildId)) {
|
|
794
|
+
this.debug(`Skipping load for destroyed player: ${guildId}`);
|
|
795
|
+
this.emit("playerSkipped", guildId, "destroyed");
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (this.restoredPlayers.has(guildId)) {
|
|
800
|
+
this.debug(`Skipping already restored player: ${guildId}`);
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
|
|
443
804
|
try {
|
|
444
805
|
const data = await this.provider.load(guildId);
|
|
445
806
|
if (!data) return false;
|
|
446
807
|
|
|
447
|
-
|
|
808
|
+
if (data.wasDestroyed === true) {
|
|
809
|
+
this.debug(`Skipping load for player marked as destroyed: ${guildId}`);
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
|
|
448
813
|
let player = this.manager.get(guildId);
|
|
449
814
|
if (!player) {
|
|
450
815
|
player = await this.manager.create(guildId, data.options);
|
|
451
816
|
}
|
|
452
817
|
|
|
453
|
-
// Restore queue
|
|
454
818
|
const queue = data.queue as SerializedQueue;
|
|
455
819
|
|
|
456
|
-
// Clear current queue
|
|
457
820
|
player.queue.clear();
|
|
458
821
|
player.queue.loop(queue.loopMode);
|
|
459
822
|
player.queue.autoPlay(queue.autoPlay);
|
|
460
823
|
|
|
461
|
-
// Restore tracks
|
|
462
824
|
if (queue.tracks.length > 0) {
|
|
463
|
-
const tracks = queue.tracks.map((t
|
|
825
|
+
const tracks = queue.tracks.map((t) => this.deserializeTrack(t));
|
|
464
826
|
player.queue.addMultiple(tracks);
|
|
465
827
|
}
|
|
466
828
|
|
|
467
|
-
// Restore current track if exists
|
|
468
829
|
if (queue.current && player.connection) {
|
|
469
830
|
const currentTrack = this.deserializeTrack(queue.current);
|
|
470
831
|
player.queue.willNextTrack(currentTrack);
|
|
471
832
|
|
|
472
|
-
// Restore playback position if requested
|
|
473
833
|
if (restorePosition && queue.position && queue.position > 0) {
|
|
474
834
|
await player.refreshPlayerResource(true, queue.position);
|
|
475
835
|
} else {
|
|
@@ -477,10 +837,8 @@ export class PersistenceManager extends EventEmitter {
|
|
|
477
837
|
}
|
|
478
838
|
}
|
|
479
839
|
|
|
480
|
-
// Restore volume
|
|
481
840
|
player.setVolume(data.volume);
|
|
482
841
|
|
|
483
|
-
// Restore filters - safely access through public method
|
|
484
842
|
if (data.filters && data.filters.length > 0) {
|
|
485
843
|
try {
|
|
486
844
|
const filterManager = (player as any).filter;
|
|
@@ -492,6 +850,9 @@ export class PersistenceManager extends EventEmitter {
|
|
|
492
850
|
}
|
|
493
851
|
}
|
|
494
852
|
|
|
853
|
+
this.restoredPlayers.add(guildId);
|
|
854
|
+
await this.clearDestroyedStatus(guildId);
|
|
855
|
+
|
|
495
856
|
this.debug(`Loaded player: ${guildId}`);
|
|
496
857
|
this.emit("playerLoaded", guildId, data);
|
|
497
858
|
return true;
|
|
@@ -503,20 +864,36 @@ export class PersistenceManager extends EventEmitter {
|
|
|
503
864
|
}
|
|
504
865
|
|
|
505
866
|
/**
|
|
506
|
-
* Load all saved players
|
|
867
|
+
* Load all saved players with auto-restore logic
|
|
507
868
|
*/
|
|
508
869
|
async loadAll(restorePosition: boolean = true): Promise<Map<string, boolean>> {
|
|
509
870
|
if (!this.options.enabled) return new Map();
|
|
510
871
|
|
|
872
|
+
await this.loadDestroyedStatus();
|
|
873
|
+
|
|
511
874
|
const results = new Map<string, boolean>();
|
|
512
875
|
|
|
513
876
|
try {
|
|
514
877
|
const keys = await this.provider.list();
|
|
515
|
-
|
|
878
|
+
const playerKeys = keys.filter((k) => k !== "__destroyed_players__");
|
|
879
|
+
this.debug(`Found ${playerKeys.length} saved players`);
|
|
880
|
+
|
|
881
|
+
if (this.options.autoRestoreOnRestart) {
|
|
882
|
+
this.debug(`Auto-restore enabled, restoring players after ${this.options.restoreDelay}ms delay...`);
|
|
883
|
+
|
|
884
|
+
if (this.options.restoreDelay && this.options.restoreDelay > 0) {
|
|
885
|
+
await new Promise((resolve) => setTimeout(resolve, this.options.restoreDelay));
|
|
886
|
+
}
|
|
516
887
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
888
|
+
for (const guildId of playerKeys) {
|
|
889
|
+
const success = await this.loadPlayer(guildId, restorePosition, true);
|
|
890
|
+
results.set(guildId, success);
|
|
891
|
+
}
|
|
892
|
+
} else {
|
|
893
|
+
for (const guildId of playerKeys) {
|
|
894
|
+
const success = await this.loadPlayer(guildId, restorePosition, true);
|
|
895
|
+
results.set(guildId, success);
|
|
896
|
+
}
|
|
520
897
|
}
|
|
521
898
|
|
|
522
899
|
this.emit("loadedAll", results);
|
|
@@ -528,6 +905,53 @@ export class PersistenceManager extends EventEmitter {
|
|
|
528
905
|
return results;
|
|
529
906
|
}
|
|
530
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Mark a player as destroyed
|
|
910
|
+
*/
|
|
911
|
+
async markPlayerDestroyed(guildId: string, reason?: string): Promise<void> {
|
|
912
|
+
this.destroyedPlayers.set(guildId, {
|
|
913
|
+
guildId,
|
|
914
|
+
destroyedAt: Date.now(),
|
|
915
|
+
reason: reason || "manual_destroy",
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
try {
|
|
919
|
+
const data = await this.provider.load(guildId);
|
|
920
|
+
if (data) {
|
|
921
|
+
data.wasDestroyed = true;
|
|
922
|
+
data.destroyedAt = Date.now();
|
|
923
|
+
await this.provider.save(guildId, data, this.options.compress);
|
|
924
|
+
}
|
|
925
|
+
} catch (error) {
|
|
926
|
+
this.debug(`Failed to mark player data as destroyed: ${guildId}`, error);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
await this.saveDestroyedStatus();
|
|
930
|
+
this.debug(`Marked player as destroyed (won't restore on restart): ${guildId}`);
|
|
931
|
+
this.emit("playerMarkedDestroyed", guildId);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Clear destroyed status for a player
|
|
936
|
+
*/
|
|
937
|
+
async clearDestroyed(guildId: string): Promise<void> {
|
|
938
|
+
await this.clearDestroyedStatus(guildId);
|
|
939
|
+
|
|
940
|
+
try {
|
|
941
|
+
const data = await this.provider.load(guildId);
|
|
942
|
+
if (data) {
|
|
943
|
+
data.wasDestroyed = false;
|
|
944
|
+
delete data.destroyedAt;
|
|
945
|
+
await this.provider.save(guildId, data, this.options.compress);
|
|
946
|
+
}
|
|
947
|
+
} catch (error) {
|
|
948
|
+
this.debug(`Failed to clear destroyed flag in data: ${guildId}`, error);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
this.debug(`Cleared destroyed status for: ${guildId}`);
|
|
952
|
+
this.emit("playerDestroyedCleared", guildId);
|
|
953
|
+
}
|
|
954
|
+
|
|
531
955
|
/**
|
|
532
956
|
* Delete a player's saved data
|
|
533
957
|
*/
|
|
@@ -536,6 +960,8 @@ export class PersistenceManager extends EventEmitter {
|
|
|
536
960
|
|
|
537
961
|
try {
|
|
538
962
|
await this.provider.delete(guildId);
|
|
963
|
+
await this.clearDestroyedStatus(guildId);
|
|
964
|
+
this.restoredPlayers.delete(guildId);
|
|
539
965
|
this.debug(`Deleted saved data for: ${guildId}`);
|
|
540
966
|
this.emit("playerDeleted", guildId);
|
|
541
967
|
return true;
|
|
@@ -554,7 +980,67 @@ export class PersistenceManager extends EventEmitter {
|
|
|
554
980
|
return false;
|
|
555
981
|
}
|
|
556
982
|
|
|
557
|
-
|
|
983
|
+
const success = await (this.provider as FileProvider).restoreBackup(guildId, timestamp);
|
|
984
|
+
if (success) {
|
|
985
|
+
await this.clearDestroyed(guildId);
|
|
986
|
+
}
|
|
987
|
+
return success;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Clean all backups for a specific player
|
|
992
|
+
*/
|
|
993
|
+
async cleanBackupsForPlayer(guildId: string): Promise<number> {
|
|
994
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
995
|
+
this.debug("Backup cleanup only supported for file provider");
|
|
996
|
+
return 0;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const deleted = await (this.provider as FileProvider).cleanAllBackupsForPlayer(guildId);
|
|
1000
|
+
this.debug(`Cleaned ${deleted} backups for player: ${guildId}`);
|
|
1001
|
+
this.emit("backupsCleaned", guildId, deleted);
|
|
1002
|
+
return deleted;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Clean all backups
|
|
1007
|
+
*/
|
|
1008
|
+
async cleanAllBackups(): Promise<number> {
|
|
1009
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
1010
|
+
this.debug("Backup cleanup only supported for file provider");
|
|
1011
|
+
return 0;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const deleted = await (this.provider as FileProvider).cleanAllBackups();
|
|
1015
|
+
this.debug(`Cleaned all ${deleted} backups`);
|
|
1016
|
+
this.emit("allBackupsCleaned", deleted);
|
|
1017
|
+
return deleted;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Get backup statistics
|
|
1022
|
+
*/
|
|
1023
|
+
getBackupStats(): {
|
|
1024
|
+
totalBackups: number;
|
|
1025
|
+
totalSizeMB: number;
|
|
1026
|
+
oldestBackup: Date | null;
|
|
1027
|
+
newestBackup: Date | null;
|
|
1028
|
+
backupsByPlayer: Record<string, number>;
|
|
1029
|
+
} | null {
|
|
1030
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
1031
|
+
this.debug("Backup stats only supported for file provider");
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const stats = (this.provider as FileProvider).getBackupStats();
|
|
1036
|
+
|
|
1037
|
+
return {
|
|
1038
|
+
totalBackups: stats.totalBackups,
|
|
1039
|
+
totalSizeMB: stats.totalSize / 1024 / 1024,
|
|
1040
|
+
oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup) : null,
|
|
1041
|
+
newestBackup: stats.newestBackup ? new Date(stats.newestBackup) : null,
|
|
1042
|
+
backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
|
|
1043
|
+
};
|
|
558
1044
|
}
|
|
559
1045
|
|
|
560
1046
|
/**
|
|
@@ -567,6 +1053,21 @@ export class PersistenceManager extends EventEmitter {
|
|
|
567
1053
|
}
|
|
568
1054
|
|
|
569
1055
|
await this.saveAll();
|
|
1056
|
+
await this.saveDestroyedStatus();
|
|
570
1057
|
this.debug("Persistence manager shut down");
|
|
571
1058
|
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Get list of destroyed players
|
|
1062
|
+
*/
|
|
1063
|
+
getDestroyedPlayers(): DestroyedRecord[] {
|
|
1064
|
+
return Array.from(this.destroyedPlayers.values());
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Check if auto-restore is enabled
|
|
1069
|
+
*/
|
|
1070
|
+
isAutoRestoreEnabled(): boolean {
|
|
1071
|
+
return this.options.autoRestoreOnRestart === true;
|
|
1072
|
+
}
|
|
572
1073
|
}
|