mysql-memory-server 1.4.3 → 1.5.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/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Download with your package manager of choice. The package name is `mysql-memory-
|
|
|
10
10
|
|
|
11
11
|
#### Requirements
|
|
12
12
|
|
|
13
|
-
- Node.js 16.6.0
|
|
13
|
+
- Node.js >=16.6.0 or Bun >= 1.0.0
|
|
14
14
|
- macOS 13+, Windows, or Linux (This package is only tested on Ubuntu 20.04, 22.04, 24.04, and Fedora 40. Other Linux distributions may or may not work at this time.)
|
|
15
15
|
|
|
16
16
|
#### Requirements for downloaded MySQL versions
|
|
@@ -186,6 +186,14 @@ Default: false
|
|
|
186
186
|
|
|
187
187
|
Description: This option only applies if the system-installed MySQL version is lower than the oldest supported MySQL version for this package (8.0.20) and the `version` option is not defined. If set to `true`, this package will use the latest version of MySQL instead of the system-installed version. If `false`, the package will throw an error.
|
|
188
188
|
|
|
189
|
+
- `downloadRetries: number`
|
|
190
|
+
|
|
191
|
+
Required: No
|
|
192
|
+
|
|
193
|
+
Default: 3
|
|
194
|
+
|
|
195
|
+
Description: The number of times to try to download a MySQL binary before giving up and rejecting the `createDB()` promise.
|
|
196
|
+
|
|
189
197
|
***
|
|
190
198
|
### :warning: Internal Options :warning:
|
|
191
199
|
|
package/dist/src/index.js
CHANGED
|
@@ -52,7 +52,8 @@ async function createDB(opts) {
|
|
|
52
52
|
ignoreUnsupportedSystemVersion: false,
|
|
53
53
|
port: 0,
|
|
54
54
|
xPort: 0,
|
|
55
|
-
binaryDirectoryPath: `${os.tmpdir()}/mysqlmsn/binaries
|
|
55
|
+
binaryDirectoryPath: `${os.tmpdir()}/mysqlmsn/binaries`,
|
|
56
|
+
downloadRetries: 10
|
|
56
57
|
};
|
|
57
58
|
const options = { ...defaultOptions, ...opts };
|
|
58
59
|
const logger = new Logger_1.default(options.logLevel);
|
|
@@ -75,12 +75,37 @@ function downloadVersions() {
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
function downloadFromCDN(url, downloadLocation, logger) {
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
78
|
+
return new Promise(async (resolve, reject) => {
|
|
79
|
+
if (fs.existsSync(downloadLocation)) {
|
|
80
|
+
logger.warn('Removing item at downloadLocation:', downloadLocation, 'so the MySQL binary archive can be stored there. This is probably because a previous download/extraction failed.');
|
|
81
|
+
await fsPromises.rm(downloadLocation, { recursive: true, force: true });
|
|
82
|
+
}
|
|
79
83
|
const fileStream = fs.createWriteStream(downloadLocation);
|
|
80
84
|
let error;
|
|
81
85
|
fileStream.on('open', () => {
|
|
82
86
|
const request = https.get(url, (response) => {
|
|
83
|
-
response.
|
|
87
|
+
if (response.statusCode !== 200) {
|
|
88
|
+
fileStream.close((err) => {
|
|
89
|
+
if (err) {
|
|
90
|
+
logger.error('An error occurred while closing the fileStream for non-200 status code. The error was:', err);
|
|
91
|
+
}
|
|
92
|
+
fs.unlink(downloadLocation, (unlinkErr) => {
|
|
93
|
+
if (unlinkErr) {
|
|
94
|
+
logger.error('An error occurred while deleting downloadLocation after non-200 status code download attempt. The error was:', err);
|
|
95
|
+
}
|
|
96
|
+
logger.error('Received status code:', response.statusCode, 'while downloading MySQL binary.');
|
|
97
|
+
reject(`Received status code ${response.statusCode} while downloading MySQL binary.`);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
response.pipe(fileStream);
|
|
103
|
+
fileStream.on('finish', () => {
|
|
104
|
+
if (!error) {
|
|
105
|
+
resolve();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
84
109
|
});
|
|
85
110
|
request.on('error', (err) => {
|
|
86
111
|
error = err;
|
|
@@ -95,11 +120,6 @@ function downloadFromCDN(url, downloadLocation, logger) {
|
|
|
95
120
|
});
|
|
96
121
|
});
|
|
97
122
|
});
|
|
98
|
-
fileStream.on('finish', () => {
|
|
99
|
-
if (!error) {
|
|
100
|
-
resolve();
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
123
|
fileStream.on('error', (err) => {
|
|
104
124
|
error = err;
|
|
105
125
|
logger.error(err);
|
|
@@ -116,6 +136,10 @@ function downloadFromCDN(url, downloadLocation, logger) {
|
|
|
116
136
|
}
|
|
117
137
|
function extractBinary(url, archiveLocation, extractedLocation, logger) {
|
|
118
138
|
return new Promise(async (resolve, reject) => {
|
|
139
|
+
if (fs.existsSync(extractedLocation)) {
|
|
140
|
+
logger.warn('Removing item at extractedLocation:', extractedLocation, 'so the MySQL binary can be stored there. This is probably because a previous download/extraction failed.');
|
|
141
|
+
await fsPromises.rm(extractedLocation, { recursive: true, force: true });
|
|
142
|
+
}
|
|
119
143
|
const lastDashIndex = url.lastIndexOf('-');
|
|
120
144
|
const fileExtension = url.slice(lastDashIndex).split('.').splice(1).join('.');
|
|
121
145
|
await fsPromises.mkdir(extractedLocation, { recursive: true });
|
|
@@ -167,7 +191,8 @@ function extractBinary(url, archiveLocation, extractedLocation, logger) {
|
|
|
167
191
|
resolve(`${extractedLocation}/mysql/bin/mysqld`);
|
|
168
192
|
}
|
|
169
193
|
}).catch(error => {
|
|
170
|
-
|
|
194
|
+
logger.error(`An error occurred while extracting the tar file. Please make sure tar is installed and there is enough storage space for the extraction. The error was: ${error}`);
|
|
195
|
+
reject(error);
|
|
171
196
|
});
|
|
172
197
|
});
|
|
173
198
|
}
|
|
@@ -190,8 +215,7 @@ function downloadBinary(binaryInfo, options, logger) {
|
|
|
190
215
|
let releaseFunction;
|
|
191
216
|
while (true) {
|
|
192
217
|
try {
|
|
193
|
-
|
|
194
|
-
releaseFunction = (0, proper_lockfile_1.lockSync)(extractedPath);
|
|
218
|
+
releaseFunction = (0, proper_lockfile_1.lockSync)(extractedPath, { realpath: false });
|
|
195
219
|
break;
|
|
196
220
|
}
|
|
197
221
|
catch (e) {
|
|
@@ -210,54 +234,82 @@ function downloadBinary(binaryInfo, options, logger) {
|
|
|
210
234
|
}
|
|
211
235
|
}
|
|
212
236
|
//The code below only runs if the lock has been acquired by us
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await extractBinary(url, archivePath, extractedPath, logger);
|
|
216
|
-
}
|
|
217
|
-
catch (e) {
|
|
237
|
+
let downloadTries = 0;
|
|
238
|
+
do {
|
|
218
239
|
try {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
240
|
+
downloadTries++;
|
|
241
|
+
await downloadFromCDN(url, archivePath, logger);
|
|
242
|
+
await extractBinary(url, archivePath, extractedPath, logger);
|
|
243
|
+
break;
|
|
223
244
|
}
|
|
224
245
|
catch (e) {
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
finally {
|
|
246
|
+
//Delete generated files since either download or extraction failed
|
|
228
247
|
try {
|
|
229
|
-
|
|
248
|
+
await Promise.all([
|
|
249
|
+
fsPromises.rm(extractedPath, { force: true, recursive: true }),
|
|
250
|
+
fsPromises.rm(archivePath, { force: true, recursive: true })
|
|
251
|
+
]);
|
|
230
252
|
}
|
|
231
253
|
catch (e) {
|
|
232
|
-
logger.error('An error occurred while
|
|
254
|
+
logger.error('An error occurred while deleting extractedPath and/or archivePath:', e);
|
|
255
|
+
}
|
|
256
|
+
if (downloadTries >= options.downloadRetries) {
|
|
257
|
+
//Only reject if we have met the downloadRetries limit
|
|
258
|
+
try {
|
|
259
|
+
releaseFunction();
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
logger.error('An error occurred while releasing lock after downloadRetries exhaustion. The error was:', e);
|
|
263
|
+
}
|
|
264
|
+
logger.error('downloadRetries have been exceeded. Aborting download.');
|
|
265
|
+
return reject(e);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.warn(`An error was encountered during the binary download process. Retrying for retry ${downloadTries}/${options.downloadRetries}. The error was:`, e);
|
|
233
269
|
}
|
|
234
|
-
return reject(e);
|
|
235
270
|
}
|
|
236
|
-
}
|
|
271
|
+
} while (downloadTries < options.downloadRetries);
|
|
237
272
|
try {
|
|
238
273
|
releaseFunction();
|
|
239
274
|
}
|
|
240
275
|
catch (e) {
|
|
241
|
-
|
|
276
|
+
logger.error('An error occurred while releasing lock after successful binary download. The error was:', e);
|
|
242
277
|
}
|
|
243
278
|
return resolve(binaryPath);
|
|
244
279
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
280
|
+
else {
|
|
281
|
+
let downloadTries = 0;
|
|
282
|
+
do {
|
|
283
|
+
const uuid = (0, crypto_1.randomUUID)();
|
|
284
|
+
const zipFilepath = `${dirpath}/${uuid}.${fileExtension}`;
|
|
285
|
+
logger.log('Binary filepath:', zipFilepath);
|
|
286
|
+
const extractedPath = `${dirpath}/${uuid}`;
|
|
287
|
+
try {
|
|
288
|
+
downloadTries++;
|
|
289
|
+
await downloadFromCDN(url, zipFilepath, logger);
|
|
290
|
+
const binaryPath = await extractBinary(url, zipFilepath, extractedPath, logger);
|
|
291
|
+
return resolve(binaryPath);
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
//Delete generated files since either download or extraction failed
|
|
295
|
+
try {
|
|
296
|
+
await Promise.all([
|
|
297
|
+
fsPromises.rm(extractedPath, { force: true, recursive: true }),
|
|
298
|
+
fsPromises.rm(zipFilepath, { force: true, recursive: true })
|
|
299
|
+
]);
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
logger.error('An error occurred while deleting extractedPath and/or archivePath:', e);
|
|
303
|
+
}
|
|
304
|
+
if (downloadTries >= options.downloadRetries) {
|
|
305
|
+
//Only reject if we have met the downloadRetries limit
|
|
306
|
+
return reject(e);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
console.warn(`An error was encountered during the binary download process. Retrying for retry ${downloadTries}/${options.downloadRetries}. The error was:`, e);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} while (downloadTries < options.downloadRetries);
|
|
261
313
|
}
|
|
262
314
|
});
|
|
263
315
|
}
|
|
@@ -4,7 +4,6 @@ declare class Executor {
|
|
|
4
4
|
#private;
|
|
5
5
|
logger: Logger;
|
|
6
6
|
constructor(logger: Logger);
|
|
7
|
-
deleteDatabaseDirectory(path: string): Promise<void>;
|
|
8
7
|
getMySQLVersion(preferredVersion?: string): Promise<InstalledMySQLVersion | null>;
|
|
9
8
|
startMySQL(options: InternalServerOptions, binaryFilepath: string): Promise<MySQLDB>;
|
|
10
9
|
}
|
|
@@ -30,7 +30,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
30
30
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
31
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
32
|
};
|
|
33
|
-
var _Executor_instances, _Executor_executeFile, _Executor_killProcess, _Executor_startMySQLProcess, _Executor_setupDataDirectories;
|
|
33
|
+
var _Executor_instances, _Executor_executeFile, _Executor_killProcess, _Executor_deleteDatabaseDirectory, _Executor_startMySQLProcess, _Executor_setupDataDirectories;
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
35
|
const child_process_1 = require("child_process");
|
|
36
36
|
const semver_1 = require("semver");
|
|
@@ -47,29 +47,6 @@ class Executor {
|
|
|
47
47
|
_Executor_instances.add(this);
|
|
48
48
|
this.logger = logger;
|
|
49
49
|
}
|
|
50
|
-
async deleteDatabaseDirectory(path) {
|
|
51
|
-
let retries = 0;
|
|
52
|
-
//Maximum wait of 10 seconds | 500ms * 20 retries = 10,000ms = 10 seconds
|
|
53
|
-
const waitTime = 500;
|
|
54
|
-
const maxRetries = 20;
|
|
55
|
-
//Since the database processes are killed instantly (SIGKILL) sometimes the database file handles may still be open
|
|
56
|
-
//This would cause an EBUSY error. Retrying the deletions for 10 seconds should give the OS enough time to close
|
|
57
|
-
//the file handles.
|
|
58
|
-
while (retries <= maxRetries) {
|
|
59
|
-
try {
|
|
60
|
-
await fsPromises.rm(path, { recursive: true, force: true });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
catch (e) {
|
|
64
|
-
if (retries === maxRetries) {
|
|
65
|
-
throw e;
|
|
66
|
-
}
|
|
67
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
68
|
-
retries++;
|
|
69
|
-
this.logger.log('DB data directory deletion failed. Now on retry', retries);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
50
|
getMySQLVersion(preferredVersion) {
|
|
74
51
|
return new Promise(async (resolve, reject) => {
|
|
75
52
|
if (process.platform === 'win32') {
|
|
@@ -182,6 +159,28 @@ _Executor_instances = new WeakSet(), _Executor_executeFile = function _Executor_
|
|
|
182
159
|
killed = process.kill();
|
|
183
160
|
}
|
|
184
161
|
return killed;
|
|
162
|
+
}, _Executor_deleteDatabaseDirectory = async function _Executor_deleteDatabaseDirectory(path) {
|
|
163
|
+
let retries = 0;
|
|
164
|
+
//Maximum wait of 10 seconds | 500ms * 20 retries = 10,000ms = 10 seconds
|
|
165
|
+
const waitTime = 500;
|
|
166
|
+
const maxRetries = 20;
|
|
167
|
+
//Since the database processes are killed instantly (SIGKILL) sometimes the database file handles may still be open
|
|
168
|
+
//This would cause an EBUSY error. Retrying the deletions for 10 seconds should give the OS enough time to close
|
|
169
|
+
//the file handles.
|
|
170
|
+
while (retries <= maxRetries) {
|
|
171
|
+
try {
|
|
172
|
+
await fsPromises.rm(path, { recursive: true, force: true });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (retries === maxRetries) {
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
180
|
+
retries++;
|
|
181
|
+
this.logger.log('DB data directory deletion failed. Now on retry', retries);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
185
184
|
}, _Executor_startMySQLProcess = function _Executor_startMySQLProcess(options, port, mySQLXPort, datadir, dbPath, binaryFilepath) {
|
|
186
185
|
const errors = [];
|
|
187
186
|
const logFile = `${dbPath}/log.log`;
|
|
@@ -207,7 +206,7 @@ _Executor_instances = new WeakSet(), _Executor_executeFile = function _Executor_
|
|
|
207
206
|
if (portIssue || xPortIssue) {
|
|
208
207
|
this.logger.log('Error log when exiting for port in use error:', errorLog);
|
|
209
208
|
try {
|
|
210
|
-
await this.
|
|
209
|
+
await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_deleteDatabaseDirectory).call(this, options.dbPath);
|
|
211
210
|
}
|
|
212
211
|
catch (e) {
|
|
213
212
|
this.logger.error(e);
|
|
@@ -217,7 +216,7 @@ _Executor_instances = new WeakSet(), _Executor_executeFile = function _Executor_
|
|
|
217
216
|
}
|
|
218
217
|
try {
|
|
219
218
|
if (options.deleteDBAfterStopped) {
|
|
220
|
-
await this.
|
|
219
|
+
await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_deleteDatabaseDirectory).call(this, dbPath);
|
|
221
220
|
}
|
|
222
221
|
}
|
|
223
222
|
catch (e) {
|
|
@@ -407,7 +406,7 @@ _Executor_instances = new WeakSet(), _Executor_executeFile = function _Executor_
|
|
|
407
406
|
}
|
|
408
407
|
//Retry setting up directory now that libaio has been copied
|
|
409
408
|
this.logger.log('Retrying directory setup');
|
|
410
|
-
await this.
|
|
409
|
+
await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_deleteDatabaseDirectory).call(this, datadir);
|
|
411
410
|
await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_setupDataDirectories).call(this, options, binaryFilepath, datadir, false);
|
|
412
411
|
return;
|
|
413
412
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type ServerOptions = {
|
|
|
15
15
|
port?: number;
|
|
16
16
|
xPort?: number;
|
|
17
17
|
binaryDirectoryPath?: string;
|
|
18
|
+
downloadRetries?: number;
|
|
18
19
|
};
|
|
19
20
|
export type InternalServerOptions = {
|
|
20
21
|
version?: string;
|
|
@@ -31,6 +32,7 @@ export type InternalServerOptions = {
|
|
|
31
32
|
port: number;
|
|
32
33
|
xPort: number;
|
|
33
34
|
binaryDirectoryPath: string;
|
|
35
|
+
downloadRetries: number;
|
|
34
36
|
};
|
|
35
37
|
export type ExecutorOptions = {
|
|
36
38
|
logLevel: LOG_LEVEL;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mysql-memory-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Spin up an ephemeral MySQL database from your JavaScript code",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -21,22 +21,23 @@
|
|
|
21
21
|
"stress": "jest --runTestsByPath stress-tests/stress.test.ts --setupFilesAfterEnv ./ciSetup.js --verbose"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": ">=16.6.0"
|
|
24
|
+
"node": ">=16.6.0",
|
|
25
|
+
"bun": ">=1.0.0"
|
|
25
26
|
},
|
|
26
27
|
"author": "Sebastian-Webster",
|
|
27
28
|
"license": "MIT",
|
|
28
29
|
"devDependencies": {
|
|
29
|
-
"@babel/core": "^7.
|
|
30
|
-
"@babel/preset-env": "^7.25.
|
|
31
|
-
"@babel/preset-typescript": "^7.
|
|
30
|
+
"@babel/core": "^7.25.8",
|
|
31
|
+
"@babel/preset-env": "^7.25.8",
|
|
32
|
+
"@babel/preset-typescript": "^7.25.7",
|
|
32
33
|
"@types/adm-zip": "^0.5.5",
|
|
33
|
-
"@types/node": "^22.5
|
|
34
|
+
"@types/node": "^22.7.5",
|
|
34
35
|
"@types/proper-lockfile": "^4.1.4",
|
|
35
36
|
"@types/semver": "^7.5.8",
|
|
36
37
|
"babel-jest": "^29.7.0",
|
|
37
38
|
"jest": "^29.7.0",
|
|
38
|
-
"mysql2": "^3.11.
|
|
39
|
-
"typescript": "^5.6.
|
|
39
|
+
"mysql2": "^3.11.3",
|
|
40
|
+
"typescript": "^5.6.3"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"adm-zip": "^0.5.16",
|