simple-backups 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 +21 -0
- package/README.md +158 -0
- package/package.json +33 -0
- package/src/SimpleBackups.js +370 -0
- package/src/index.js +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kartik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# SimpleBackups: A Simple Package To Automatically Create And Prune Backups.
|
|
2
|
+
|
|
3
|
+
<div align="left">
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/simple-backups)
|
|
6
|
+
[](https://www.npmjs.com/package/simple-backups)
|
|
7
|
+
[](https://github.com/kartikk221/simple-backups/issues)
|
|
8
|
+
[](https://github.com/kartikk221/simple-backups/stargazers)
|
|
9
|
+
[](https://github.com/kartikk221/simple-backups/blob/master/LICENSE)
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
## Motivation
|
|
14
|
+
This package aims to simplify the task of automatically creating and pruning backups from any upstream storage mechanism. This package does not care where your backups are stored and instead uses a small adapter interface to list, create and delete backups while maintaining multiple backup retention windows.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
- Simple-to-use API
|
|
18
|
+
- JSDoc Type Support
|
|
19
|
+
- Automatic Backup Creation
|
|
20
|
+
- Multi-Window Backup Retention
|
|
21
|
+
- CPU & Memory Efficient
|
|
22
|
+
- No Dependencies
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
SimpleBackups can be installed using node package manager (`npm`)
|
|
26
|
+
```
|
|
27
|
+
npm i simple-backups
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## How To Use?
|
|
31
|
+
Below is a small snippet that shows how to use a `SimpleBackups` instance.
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
import { SimpleBackups } from 'simple-backups';
|
|
35
|
+
|
|
36
|
+
const hour = 1000 * 60 * 60;
|
|
37
|
+
const day = hour * 24;
|
|
38
|
+
|
|
39
|
+
// Create a simple backups instance which keeps 24 hourly backups and 30 daily backups
|
|
40
|
+
const backups = new SimpleBackups({
|
|
41
|
+
windows: [
|
|
42
|
+
{ interval_ms: hour, limit: 24 },
|
|
43
|
+
{ interval_ms: day, limit: 30 }
|
|
44
|
+
],
|
|
45
|
+
adapters: {
|
|
46
|
+
async list() {
|
|
47
|
+
// Return every backup currently stored in the upstream storage mechanism
|
|
48
|
+
// Each backup must have a unique id and a timestamp in milliseconds
|
|
49
|
+
return await storage.list_backups();
|
|
50
|
+
},
|
|
51
|
+
async create() {
|
|
52
|
+
// Create a new backup in the upstream storage mechanism
|
|
53
|
+
const backup = await storage.create_backup();
|
|
54
|
+
|
|
55
|
+
// Return the backup record so SimpleBackups can track it
|
|
56
|
+
return {
|
|
57
|
+
id: backup.id,
|
|
58
|
+
timestamp: backup.timestamp
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
async delete(backup) {
|
|
62
|
+
// Attempt to delete the backup from the upstream storage mechanism
|
|
63
|
+
// Return true only if the backup was successfully deleted
|
|
64
|
+
return await storage.delete_backup(backup.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Listen for newly created backups
|
|
70
|
+
backups.on('create', (backup) => {
|
|
71
|
+
console.log('Created backup:', backup.id);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Listen for successfully deleted backups
|
|
75
|
+
backups.on('delete', (backup) => {
|
|
76
|
+
console.log('Deleted backup:', backup.id);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Listen for adapter or validation errors
|
|
80
|
+
backups.on('error', (error) => {
|
|
81
|
+
console.error(error);
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## SimpleBackups
|
|
86
|
+
Below is a breakdown of the `SimpleBackups` class.
|
|
87
|
+
|
|
88
|
+
#### Constructor Parameters
|
|
89
|
+
* `new SimpleBackups(Object: options)`: Creates a new SimpleBackups instance with the provided `options`.
|
|
90
|
+
* `options` [`Object`]: Constructor options for this instance.
|
|
91
|
+
* `adapters` [`BackupAdapters`]: Adapter methods used to list, create and delete backups from the upstream storage mechanism.
|
|
92
|
+
* `windows` [`BackupWindow[]`]: Retention windows used to determine when backups should be created and which backups should be kept.
|
|
93
|
+
* **Note!** the SimpleBackups instance starts automatically when constructed.
|
|
94
|
+
* **Note!** the smallest `interval_ms` from the provided windows is used as the internal tick interval.
|
|
95
|
+
* **Note!** all window `interval_ms` and `limit` values must be positive finite integers.
|
|
96
|
+
|
|
97
|
+
#### SimpleBackups Properties
|
|
98
|
+
| Property | Type | Description |
|
|
99
|
+
| :-------- | :------- | :------------------------- |
|
|
100
|
+
| `backups` | `Backup[]` | The most recently listed backups from the upstream storage mechanism. |
|
|
101
|
+
| `destroyed` | `Boolean` | Whether this SimpleBackups instance has been destroyed. |
|
|
102
|
+
|
|
103
|
+
#### SimpleBackups Methods
|
|
104
|
+
* `destroy()`: Destroys the SimpleBackups instance and stops all internal intervals.
|
|
105
|
+
* **Note** this method also removes all event listeners from the instance.
|
|
106
|
+
* `on(String: event, Function: listener)`: Adds a listener for a SimpleBackups event.
|
|
107
|
+
* **Returns** the SimpleBackups instance.
|
|
108
|
+
* `once(String: event, Function: listener)`: Adds a one-time listener for a SimpleBackups event.
|
|
109
|
+
* **Returns** the SimpleBackups instance.
|
|
110
|
+
* `off(String: event, Function: listener)`: Removes a listener for a SimpleBackups event.
|
|
111
|
+
* **Returns** the SimpleBackups instance.
|
|
112
|
+
|
|
113
|
+
#### SimpleBackups Events
|
|
114
|
+
* [`create`]: The `create` event is emitted whenever a backup is successfully created.
|
|
115
|
+
* **Example:** `backups.on('create', (backup) => { /* Your Code */ });`
|
|
116
|
+
* [`delete`]: The `delete` event is emitted whenever a backup is successfully deleted.
|
|
117
|
+
* **Example:** `backups.on('delete', (backup) => { /* Your Code */ });`
|
|
118
|
+
* [`error`]: The `error` event is emitted whenever an adapter or validation error occurs.
|
|
119
|
+
* **Example:** `backups.on('error', (error) => { /* Your Code */ });`
|
|
120
|
+
* **Note** this is a standard Node.js `error` event and should be handled by the user.
|
|
121
|
+
|
|
122
|
+
### BackupAdapters Properties
|
|
123
|
+
| Property | Type | Description |
|
|
124
|
+
| :-------- | :------- | :------------------------- |
|
|
125
|
+
| `list` | `function(): Backup[] \| Promise<Backup[]>` | Lists all backups from the upstream storage mechanism. |
|
|
126
|
+
| `create` | `function(): Backup \| Promise<Backup>` | Creates a new backup in the upstream storage mechanism. |
|
|
127
|
+
| `delete` | `function(Backup): Boolean \| Promise<Boolean>` | Attempts to delete the specified backup from the upstream storage mechanism. |
|
|
128
|
+
|
|
129
|
+
### BackupWindow Properties
|
|
130
|
+
| Property | Type | Description |
|
|
131
|
+
| :-------- | :------- | :------------------------- |
|
|
132
|
+
| `interval_ms` | `Number` | The interval at which this window's backups should be created in milliseconds. |
|
|
133
|
+
| `limit` | `Number` | The maximum number of backups to keep in this window. |
|
|
134
|
+
|
|
135
|
+
### Backup Properties
|
|
136
|
+
| Property | Type | Description |
|
|
137
|
+
| :-------- | :------- | :------------------------- |
|
|
138
|
+
| `id` | `String` | The unique identifier of the backup. |
|
|
139
|
+
| `timestamp` | `Number` | The timestamp at which the backup was created in milliseconds. |
|
|
140
|
+
|
|
141
|
+
## Retention Behavior
|
|
142
|
+
SimpleBackups creates a new backup when no existing backup exists within the smallest configured backup window interval. During each tick, every backup window independently selects the best backup for each time slot and stale backups which are not selected by any window are deleted.
|
|
143
|
+
|
|
144
|
+
For example, the following windows will attempt to keep up to 24 hourly backups and up to 30 daily backups.
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
windows: [
|
|
148
|
+
{ interval_ms: 1000 * 60 * 60, limit: 24 },
|
|
149
|
+
{ interval_ms: 1000 * 60 * 60 * 24, limit: 30 }
|
|
150
|
+
]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
* **Note** backups can satisfy multiple windows at the same time.
|
|
154
|
+
* **Note** SimpleBackups can only keep backups that exist and cannot create missed backups for time periods where the process was offline.
|
|
155
|
+
* **Note** backups are selected by timestamp and not by filename or storage order.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
[MIT](./LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "simple-backups",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Simple yet unopinionated zero-dependency package to easily setup automatic backups for any data source that persists to any storage mechanism.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"backup",
|
|
20
|
+
"backups",
|
|
21
|
+
"storage",
|
|
22
|
+
"scheduler",
|
|
23
|
+
"nodejs",
|
|
24
|
+
"javascript",
|
|
25
|
+
"s3-bucket",
|
|
26
|
+
"unopinionated",
|
|
27
|
+
"zero-dependency"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} Backup
|
|
5
|
+
* @property {string} id The unique identifier of the backup.
|
|
6
|
+
* @property {number} timestamp The timestamp at which the backup was created (in milliseconds).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} BackupWindow
|
|
11
|
+
* @property {number} interval_ms The interval at which this window's backups should be created (in milliseconds).
|
|
12
|
+
* @property {number} limit The maximum number of backups to keep in this window.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} BackupAdapters
|
|
17
|
+
* @property {function():Backup[]|Promise<Backup[]>} list This method **must** return an array of all backups from the upstream storage mechanism.
|
|
18
|
+
* @property {function():Backup|Promise<Backup>} create This method **must** create a new backup and persist it to the upstream storage mechanism.
|
|
19
|
+
* @property {function(Backup):boolean|Promise<boolean>} delete This method **must** attempt to delete the specified backups from the upstream storage mechanism and return a boolean indicating whether the operation was successful.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} SimpleBackupsOptions
|
|
24
|
+
* @property {BackupAdapters} adapters The backup adapters to use.
|
|
25
|
+
* @property {BackupWindow[]} windows The backup windows to use.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {'create'|'delete'} SimpleBackupsBackupEvent
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {'error'} SimpleBackupsErrorEvent
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {(backup: Backup) => void} SimpleBackupsBackupListener
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {(error: Error) => void} SimpleBackupsErrorListener
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export class SimpleBackups extends EventEmitter {
|
|
45
|
+
#destroyed = false;
|
|
46
|
+
#initialized = false;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The options for the SimpleBackups instance.
|
|
50
|
+
* @type {SimpleBackupsOptions}
|
|
51
|
+
*/
|
|
52
|
+
#options;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The intervals for the backup windows.
|
|
56
|
+
* @type {Map<string, ReturnType<typeof setInterval>>}
|
|
57
|
+
*/
|
|
58
|
+
#intervals = new Map();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The most recently listed backups from the upstream storage mechanism.
|
|
62
|
+
* @type {Backup[]}
|
|
63
|
+
*/
|
|
64
|
+
#backups = [];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The smallest backup window interval.
|
|
68
|
+
* @type {number}
|
|
69
|
+
*/
|
|
70
|
+
#smallest_interval_ms = Infinity;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Constructs a new SimpleBackups instance.
|
|
74
|
+
* @param {SimpleBackupsOptions} options
|
|
75
|
+
*/
|
|
76
|
+
constructor(options) {
|
|
77
|
+
super();
|
|
78
|
+
this.#options = options;
|
|
79
|
+
|
|
80
|
+
// Enforce valid options
|
|
81
|
+
if (!Array.isArray(this.#options?.windows) || !this.#options?.windows?.length)
|
|
82
|
+
throw new Error('You must specify at least one backup window.');
|
|
83
|
+
if (typeof this.#options?.adapters?.list !== 'function')
|
|
84
|
+
throw new Error('You must specify a backup adapters list method.');
|
|
85
|
+
if (typeof this.#options?.adapters?.create !== 'function')
|
|
86
|
+
throw new Error('You must specify a backup adapters create method.');
|
|
87
|
+
if (typeof this.#options?.adapters?.delete !== 'function')
|
|
88
|
+
throw new Error('You must specify a backup adapters delete method.');
|
|
89
|
+
|
|
90
|
+
// Find the most frequent backup interval
|
|
91
|
+
for (const window of this.#options.windows) {
|
|
92
|
+
if (
|
|
93
|
+
Number.isNaN(window?.limit) ||
|
|
94
|
+
!Number.isFinite(window?.limit) ||
|
|
95
|
+
!Number.isInteger(window?.limit) ||
|
|
96
|
+
window?.limit <= 0
|
|
97
|
+
)
|
|
98
|
+
throw new Error('Backup windows must have a positive finite limit.');
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
Number.isNaN(window?.interval_ms) ||
|
|
102
|
+
!Number.isFinite(window?.interval_ms) ||
|
|
103
|
+
!Number.isInteger(window?.interval_ms) ||
|
|
104
|
+
window?.interval_ms <= 0
|
|
105
|
+
)
|
|
106
|
+
throw new Error('Backup windows must have a positive finite interval_ms.');
|
|
107
|
+
|
|
108
|
+
if (window?.interval_ms <= this.#smallest_interval_ms) this.#smallest_interval_ms = window?.interval_ms;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Create a tick interval with the smallest interval (aka. most precision)
|
|
112
|
+
this.#intervals.set(
|
|
113
|
+
'tick',
|
|
114
|
+
setInterval(() => this.__tick(), this.#smallest_interval_ms),
|
|
115
|
+
);
|
|
116
|
+
setTimeout(() => this.__tick(), 0); // Schedule an immediate tick
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#tick_in_flight = false;
|
|
120
|
+
/**
|
|
121
|
+
* Ticks the SimpleBackups instance.
|
|
122
|
+
*/
|
|
123
|
+
async __tick() {
|
|
124
|
+
if (this.#destroyed || this.#tick_in_flight) return;
|
|
125
|
+
this.#tick_in_flight = true;
|
|
126
|
+
try {
|
|
127
|
+
// If we have not initialized yet, then list all available backups from the upstream storage mechanism and cache them in memory
|
|
128
|
+
if (!this.#initialized) {
|
|
129
|
+
try {
|
|
130
|
+
this.#backups = await this.#options.adapters.list();
|
|
131
|
+
if (!Array.isArray(this.#backups))
|
|
132
|
+
throw new Error('Backup adapters list method must return an array of backups.');
|
|
133
|
+
|
|
134
|
+
// Validate the backups
|
|
135
|
+
const seen_ids = new Set();
|
|
136
|
+
for (const backup of this.#backups) {
|
|
137
|
+
if (!backup?.id || typeof backup?.id !== 'string' || seen_ids.has(backup?.id))
|
|
138
|
+
throw new Error(
|
|
139
|
+
'Backup adapters list method must return an array of backups with unique IDs.',
|
|
140
|
+
);
|
|
141
|
+
seen_ids.add(backup.id);
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
typeof backup?.timestamp !== 'number' ||
|
|
145
|
+
!Number.isFinite(backup?.timestamp) ||
|
|
146
|
+
backup?.timestamp < 0
|
|
147
|
+
)
|
|
148
|
+
throw new Error(
|
|
149
|
+
'Backup adapters list method must return an array of backups with finite timestamps.',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.#backups.sort((a, b) => a.timestamp - b.timestamp); // Sort by ascending timestamp (oldest to newest)
|
|
154
|
+
this.#initialized = true;
|
|
155
|
+
if (this.#destroyed) return; // If the instance has been destroyed, then we should stop ticking
|
|
156
|
+
} catch (error) {
|
|
157
|
+
this.emit('error', error); // Emit the error which may have occurred when listing the backups
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let now = Date.now();
|
|
163
|
+
let should_create_backup = true; // Scan all existing backups to determine if we have a backup within the smallest interval slot to determine whether we need to create a new backup
|
|
164
|
+
for (let i = 0; i < this.#backups.length; i++) {
|
|
165
|
+
const backup = this.#backups[i];
|
|
166
|
+
const backup_age = now - backup.timestamp; // Measure the age of the backup (in milliseconds)
|
|
167
|
+
if (backup_age >= 0 && backup_age < this.#smallest_interval_ms) {
|
|
168
|
+
should_create_backup = false; // We already have a backup within the smallest interval, so we don't need to create another one
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (should_create_backup) {
|
|
173
|
+
try {
|
|
174
|
+
const backup = await this.#options.adapters.create(); // Create a new backup
|
|
175
|
+
|
|
176
|
+
let already_exists = false; // Determine if a backup with the same ID already exists
|
|
177
|
+
for (const _backup of this.#backups) {
|
|
178
|
+
if (_backup.id === backup?.id) {
|
|
179
|
+
already_exists = true;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!backup?.id || typeof backup?.id !== 'string' || already_exists)
|
|
184
|
+
throw new Error('Backup adapters create method must return a backup with a unique ID.');
|
|
185
|
+
if (
|
|
186
|
+
typeof backup.timestamp !== 'number' ||
|
|
187
|
+
!Number.isFinite(backup.timestamp) ||
|
|
188
|
+
backup.timestamp < 0
|
|
189
|
+
)
|
|
190
|
+
throw new Error('Backup adapters create method must return a backup with a finite timestamp.');
|
|
191
|
+
|
|
192
|
+
this.#backups.push(backup); // Add the new backup to the list of backups in cache
|
|
193
|
+
this.#backups.sort((a, b) => a.timestamp - b.timestamp); // Sort by ascending timestamp (oldest to newest)
|
|
194
|
+
now = Date.now(); // Update the current time since we did a potentially asynchronous operation
|
|
195
|
+
this.emit('create', backup); // Emit this backup as being created
|
|
196
|
+
if (this.#destroyed) return; // If the instance has been destroyed, then we should stop ticking
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.emit('error', error); // Emit the error which may have occurred when creating the backup
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const keep_backups_ids = new Set(); // Determine which backup IDs to keep
|
|
204
|
+
for (let i = 0; i < this.#options.windows.length; i++) {
|
|
205
|
+
const best_by_slot = new Map();
|
|
206
|
+
const window = this.#options.windows[i];
|
|
207
|
+
for (let j = 0; j < this.#backups.length; j++) {
|
|
208
|
+
const backup = this.#backups[j];
|
|
209
|
+
const backup_age_ms = now - backup.timestamp;
|
|
210
|
+
if (backup_age_ms < 0) continue; // Ignore future backups (this should not even be possible?)
|
|
211
|
+
|
|
212
|
+
const slot = Math.floor(backup_age_ms / window.interval_ms); // Calculate the window slot for the backup
|
|
213
|
+
if (slot >= window.limit) continue; // Ignore backups outside this window
|
|
214
|
+
|
|
215
|
+
const target_timestamp = now - slot * window.interval_ms; // Calculate the target timestamp for this backup's ideal slot
|
|
216
|
+
const distance_to_target_ms = Math.abs(backup.timestamp - target_timestamp); // Calculate the distance between the backup's timestamp and the target timestamp
|
|
217
|
+
|
|
218
|
+
const candidate = best_by_slot.get(slot); // Measure and track the best or lowest distance to target timestamp for a given slot
|
|
219
|
+
if (!candidate || distance_to_target_ms < candidate.distance_to_target_ms) {
|
|
220
|
+
best_by_slot.set(slot, { backup, distance_to_target_ms });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const [_, { backup }] of best_by_slot) keep_backups_ids.add(backup.id); // Add the remaining best backup slot candidates to the list of backups to keep
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const stale_backups = []; // Determine which backups are stale and need to be deleted
|
|
228
|
+
const deleted_backups_ids = new Set(); // Determine which backup IDs have been deleted
|
|
229
|
+
for (let i = 0; i < this.#backups.length; i++) {
|
|
230
|
+
const backup = this.#backups[i];
|
|
231
|
+
if (!keep_backups_ids.has(backup.id)) stale_backups.push(backup);
|
|
232
|
+
}
|
|
233
|
+
for (const backup of stale_backups) {
|
|
234
|
+
try {
|
|
235
|
+
const deleted = await this.#options.adapters.delete(backup); // Attempt to delete the stale backup
|
|
236
|
+
if (deleted) {
|
|
237
|
+
this.emit('delete', backup); // Emit this backup as being deleted
|
|
238
|
+
deleted_backups_ids.add(backup.id); // If the deletion was successful, add the backup ID to the list of deleted backups
|
|
239
|
+
}
|
|
240
|
+
if (this.#destroyed) return; // If the instance has been destroyed, then we should stop ticking
|
|
241
|
+
} catch (error) {
|
|
242
|
+
this.emit('error', error); // Emit the error which may have occurred when deleting the backup
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (deleted_backups_ids.size > 0) {
|
|
246
|
+
const remaining_backups = [];
|
|
247
|
+
for (let i = 0; i < this.#backups.length; i++) {
|
|
248
|
+
const backup = this.#backups[i];
|
|
249
|
+
if (!deleted_backups_ids.has(backup.id)) remaining_backups.push(backup);
|
|
250
|
+
}
|
|
251
|
+
this.#backups = remaining_backups; // Rebuild and overwrite the backups array with the remaining backups
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
this.emit('error', error);
|
|
255
|
+
} finally {
|
|
256
|
+
this.#tick_in_flight = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @overload
|
|
262
|
+
* @param {SimpleBackupsBackupEvent} event
|
|
263
|
+
* @param {SimpleBackupsBackupListener} listener
|
|
264
|
+
* @returns {this}
|
|
265
|
+
*/
|
|
266
|
+
/**
|
|
267
|
+
* @overload
|
|
268
|
+
* @param {SimpleBackupsErrorEvent} event
|
|
269
|
+
* @param {SimpleBackupsErrorListener} listener
|
|
270
|
+
* @returns {this}
|
|
271
|
+
*/
|
|
272
|
+
/**
|
|
273
|
+
* @param {SimpleBackupsBackupEvent|SimpleBackupsErrorEvent} event
|
|
274
|
+
* @param {SimpleBackupsBackupListener|SimpleBackupsErrorListener} listener
|
|
275
|
+
* @returns {this}
|
|
276
|
+
*/
|
|
277
|
+
on(event, listener) {
|
|
278
|
+
return super.on(event, listener);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @overload
|
|
283
|
+
* @param {SimpleBackupsBackupEvent} event
|
|
284
|
+
* @param {SimpleBackupsBackupListener} listener
|
|
285
|
+
* @returns {this}
|
|
286
|
+
*/
|
|
287
|
+
/**
|
|
288
|
+
* @overload
|
|
289
|
+
* @param {SimpleBackupsErrorEvent} event
|
|
290
|
+
* @param {SimpleBackupsErrorListener} listener
|
|
291
|
+
* @returns {this}
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
294
|
+
* @param {SimpleBackupsBackupEvent|SimpleBackupsErrorEvent} event
|
|
295
|
+
* @param {SimpleBackupsBackupListener|SimpleBackupsErrorListener} listener
|
|
296
|
+
* @returns {this}
|
|
297
|
+
*/
|
|
298
|
+
once(event, listener) {
|
|
299
|
+
return super.once(event, listener);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @overload
|
|
304
|
+
* @param {SimpleBackupsBackupEvent} event
|
|
305
|
+
* @param {SimpleBackupsBackupListener} listener
|
|
306
|
+
* @returns {this}
|
|
307
|
+
*/
|
|
308
|
+
/**
|
|
309
|
+
* @overload
|
|
310
|
+
* @param {SimpleBackupsErrorEvent} event
|
|
311
|
+
* @param {SimpleBackupsErrorListener} listener
|
|
312
|
+
* @returns {this}
|
|
313
|
+
*/
|
|
314
|
+
/**
|
|
315
|
+
* @param {SimpleBackupsBackupEvent|SimpleBackupsErrorEvent} event
|
|
316
|
+
* @param {SimpleBackupsBackupListener|SimpleBackupsErrorListener} listener
|
|
317
|
+
* @returns {this}
|
|
318
|
+
*/
|
|
319
|
+
off(event, listener) {
|
|
320
|
+
return super.off(event, listener);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* @overload
|
|
325
|
+
* @param {SimpleBackupsBackupEvent} event
|
|
326
|
+
* @param {Backup} backup
|
|
327
|
+
* @returns {boolean}
|
|
328
|
+
*/
|
|
329
|
+
/**
|
|
330
|
+
* @overload
|
|
331
|
+
* @param {SimpleBackupsErrorEvent} event
|
|
332
|
+
* @param {Error} error
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
*/
|
|
335
|
+
/**
|
|
336
|
+
* @param {SimpleBackupsBackupEvent|SimpleBackupsErrorEvent} event
|
|
337
|
+
* @param {Backup|Error} value
|
|
338
|
+
* @returns {boolean}
|
|
339
|
+
*/
|
|
340
|
+
emit(event, value) {
|
|
341
|
+
return super.emit(event, value);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Destroys the SimpleBackups instance and stops all intervals.
|
|
346
|
+
*/
|
|
347
|
+
destroy() {
|
|
348
|
+
if (this.#destroyed) return;
|
|
349
|
+
this.#destroyed = true; // Only destroy once
|
|
350
|
+
|
|
351
|
+
for (const [_, interval] of this.#intervals) clearInterval(interval);
|
|
352
|
+
this.#intervals.clear(); // Clear all intervals
|
|
353
|
+
|
|
354
|
+
super.removeAllListeners(); // Remove all event listeners (which effectively destroys the EventEmitter)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Returns the current list of backups (if in cache).
|
|
359
|
+
*/
|
|
360
|
+
get backups() {
|
|
361
|
+
return [...this.#backups]; // Return a copy of the backups array
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Whether the SimpleBackups instance has been destroyed.
|
|
366
|
+
*/
|
|
367
|
+
get destroyed() {
|
|
368
|
+
return this.#destroyed;
|
|
369
|
+
}
|
|
370
|
+
}
|
package/src/index.js
ADDED