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 and newer
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.pipe(fileStream);
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
- reject(`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}`);
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
- await fsPromises.mkdir(extractedPath, { recursive: true });
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
- try {
214
- await downloadFromCDN(url, archivePath, logger);
215
- await extractBinary(url, archivePath, extractedPath, logger);
216
- }
217
- catch (e) {
237
+ let downloadTries = 0;
238
+ do {
218
239
  try {
219
- await Promise.all([
220
- fsPromises.rm(extractedPath, { force: true, recursive: true }),
221
- fsPromises.rm(archivePath, { force: true, recursive: true })
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
- logger.error('An error occurred while deleting extractedPath and/or archivePath:', e);
226
- }
227
- finally {
246
+ //Delete generated files since either download or extraction failed
228
247
  try {
229
- releaseFunction();
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 unlocking path:', e);
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
- return reject(e);
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
- const uuid = (0, crypto_1.randomUUID)();
246
- const zipFilepath = `${dirpath}/${uuid}.${fileExtension}`;
247
- logger.log('Binary filepath:', zipFilepath);
248
- const extractedPath = `${dirpath}/${uuid}`;
249
- try {
250
- await downloadFromCDN(url, zipFilepath, logger);
251
- }
252
- catch (e) {
253
- reject(e);
254
- }
255
- try {
256
- const binaryPath = await extractBinary(url, zipFilepath, extractedPath, logger);
257
- resolve(binaryPath);
258
- }
259
- catch (e) {
260
- reject(e);
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.deleteDatabaseDirectory(options.dbPath);
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.deleteDatabaseDirectory(dbPath);
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.deleteDatabaseDirectory(datadir);
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
  }
@@ -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.4.3",
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.24.9",
30
- "@babel/preset-env": "^7.25.4",
31
- "@babel/preset-typescript": "^7.24.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.4",
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.2",
39
- "typescript": "^5.6.2"
39
+ "mysql2": "^3.11.3",
40
+ "typescript": "^5.6.3"
40
41
  },
41
42
  "dependencies": {
42
43
  "adm-zip": "^0.5.16",