mysql-memory-server 1.4.2 → 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
@@ -54,7 +54,7 @@ const connection = await sql.createConnection({
54
54
  host: '127.0.0.1',
55
55
  user: db.username,
56
56
  port: db.port,
57
- database: db.dbName
57
+ database: db.dbName,
58
58
  password: ''
59
59
  })
60
60
 
@@ -186,22 +186,39 @@ 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
+
197
+ ***
198
+ ### :warning: Internal Options :warning:
199
+
200
+ The following options are only meant for internal debugging use. Their behaviour may change or they may get removed between major/minor/patch versions and they are not to be considered stable. The options below will not follow Semantic Versioning so it is advised to not use them.
201
+
189
202
  - `deleteDBAfterStopped: boolean`
190
203
 
191
204
  Required: No
192
205
 
193
206
  Default: true
194
207
 
195
- Gotchas: This option is intended to be for internal debugging purposes only and not meant for people to use. As such, this option will not follow Semantic Versioning.
196
-
197
208
  Description: Changes whether or not the database will be deleted after it has been stopped. If set to `true`, the database WILL be deleted after it has been stopped.
198
209
 
199
210
  - `dataPath: string`
200
211
 
201
212
  Required: No
202
213
 
203
- Default: `TMPDIR/mysqlmsn/dbs/UUID` (replacing TMPDIR with the OS temp directory and UUID with a UUIDv4 without seperating dashes)
204
-
205
- Gotchas: This option is intended to be for internal debugging purposes only and not meant for people to use. As such, this option will not follow Semantic Versioning.
214
+ Default: `TMPDIR/mysqlmsn/dbs/UUID` (replacing TMPDIR with the OS temp directory and UUID with a UUIDv4 without seperating dashes).
206
215
 
207
216
  Description: The folder to store database-related data in
217
+
218
+ - `binaryDirectoryPath: string`
219
+
220
+ Required: No
221
+
222
+ Default: `TMPDIR/mysqlmsn/binaries` (replacing TMPDIR with the OS temp directory)
223
+
224
+ Description: The folder to store the MySQL binaries when they are downloaded from the CDN.
package/dist/src/index.js CHANGED
@@ -51,7 +51,9 @@ async function createDB(opts) {
51
51
  dbPath: (0, path_1.normalize)(`${os.tmpdir()}/mysqlmsn/dbs/${(0, crypto_1.randomUUID)().replace(/-/g, '')}`),
52
52
  ignoreUnsupportedSystemVersion: false,
53
53
  port: 0,
54
- xPort: 0
54
+ xPort: 0,
55
+ binaryDirectoryPath: `${os.tmpdir()}/mysqlmsn/binaries`,
56
+ downloadRetries: 10
55
57
  };
56
58
  const options = { ...defaultOptions, ...opts };
57
59
  const logger = new Logger_1.default(options.logLevel);
@@ -73,7 +75,7 @@ async function createDB(opts) {
73
75
  let binaryFilepath;
74
76
  try {
75
77
  binaryInfo = (0, Version_1.default)(versions_json_1.default, options.version);
76
- logger.log('Downloading binary:', binaryInfo.version, 'from URL:', binaryInfo.url);
78
+ logger.log('Using MySQL binary version:', binaryInfo.version, 'from URL:', binaryInfo.url);
77
79
  }
78
80
  catch (e) {
79
81
  logger.error(e);
@@ -31,7 +31,6 @@ exports.downloadBinary = downloadBinary;
31
31
  const https = __importStar(require("https"));
32
32
  const fs = __importStar(require("fs"));
33
33
  const fsPromises = __importStar(require("fs/promises"));
34
- const os = __importStar(require("os"));
35
34
  const adm_zip_1 = __importDefault(require("adm-zip"));
36
35
  const path_1 = require("path");
37
36
  const crypto_1 = require("crypto");
@@ -52,7 +51,7 @@ function getZipData(entry) {
52
51
  }
53
52
  function handleTarExtraction(filepath, extractedPath) {
54
53
  return new Promise((resolve, reject) => {
55
- (0, child_process_1.exec)(`tar -xf ${filepath} -C ${extractedPath}`, (error, stdout, stderr) => {
54
+ (0, child_process_1.execFile)(`tar`, ['-xf', filepath, '-C', extractedPath], (error, stdout, stderr) => {
56
55
  if (error || stderr) {
57
56
  return reject(error || stderr);
58
57
  }
@@ -76,39 +75,71 @@ function downloadVersions() {
76
75
  });
77
76
  }
78
77
  function downloadFromCDN(url, downloadLocation, logger) {
79
- 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
+ }
80
83
  const fileStream = fs.createWriteStream(downloadLocation);
81
84
  let error;
82
85
  fileStream.on('open', () => {
83
86
  const request = https.get(url, (response) => {
84
- 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
+ }
85
109
  });
86
110
  request.on('error', (err) => {
87
111
  error = err;
88
112
  logger.error(err);
89
- fileStream.close();
90
- fs.unlink(downloadLocation, () => {
91
- reject(err.message);
113
+ fileStream.end(() => {
114
+ fs.unlink(downloadLocation, (unlinkError) => {
115
+ if (unlinkError) {
116
+ logger.error('An error occurred while deleting downloadLocation after an error occurred with the MySQL server binary download. The error was:', unlinkError);
117
+ }
118
+ reject(err.message);
119
+ });
92
120
  });
93
121
  });
94
122
  });
95
- fileStream.on('finish', () => {
96
- if (!error) {
97
- resolve();
98
- }
99
- });
100
123
  fileStream.on('error', (err) => {
101
124
  error = err;
102
125
  logger.error(err);
103
- fileStream.end();
104
- fs.unlink(downloadLocation, () => {
105
- reject(err.message);
126
+ fileStream.end(() => {
127
+ fs.unlink(downloadLocation, (unlinkError) => {
128
+ if (unlinkError) {
129
+ logger.error('An error occurred while deleting downloadLocation after an error occurred with the fileStream. The error was:', unlinkError);
130
+ }
131
+ reject(err.message);
132
+ });
106
133
  });
107
134
  });
108
135
  });
109
136
  }
110
137
  function extractBinary(url, archiveLocation, extractedLocation, logger) {
111
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
+ }
112
143
  const lastDashIndex = url.lastIndexOf('-');
113
144
  const fileExtension = url.slice(lastDashIndex).split('.').splice(1).join('.');
114
145
  await fsPromises.mkdir(extractedLocation, { recursive: true });
@@ -160,83 +191,125 @@ function extractBinary(url, archiveLocation, extractedLocation, logger) {
160
191
  resolve(`${extractedLocation}/mysql/bin/mysqld`);
161
192
  }
162
193
  }).catch(error => {
163
- 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);
164
196
  });
165
197
  });
166
198
  }
167
199
  function downloadBinary(binaryInfo, options, logger) {
168
200
  return new Promise(async (resolve, reject) => {
169
201
  const { url, version } = binaryInfo;
170
- const dirpath = `${os.tmpdir()}/mysqlmsn/binaries`;
202
+ const dirpath = options.binaryDirectoryPath;
171
203
  logger.log('Binary path:', dirpath);
172
204
  await fsPromises.mkdir(dirpath, { recursive: true });
173
205
  const lastDashIndex = url.lastIndexOf('-');
174
206
  const fileExtension = url.slice(lastDashIndex).split('.').splice(1).join('.');
175
207
  if (options.downloadBinaryOnce) {
176
208
  const extractedPath = `${dirpath}/${version}`;
177
- await fsPromises.mkdir(extractedPath, { recursive: true });
178
209
  const binaryPath = (0, path_1.normalize)(`${extractedPath}/mysql/bin/mysqld${process.platform === 'win32' ? '.exe' : ''}`);
179
210
  const archivePath = `${dirpath}/${version}.${fileExtension}`;
180
211
  const binaryExists = fs.existsSync(binaryPath);
181
212
  if (binaryExists) {
182
213
  return resolve(binaryPath);
183
214
  }
184
- let lockedByUs = false;
185
- try {
186
- (0, proper_lockfile_1.lockSync)(extractedPath);
187
- lockedByUs = true;
188
- await downloadFromCDN(url, archivePath, logger);
189
- await extractBinary(url, archivePath, extractedPath, logger);
215
+ let releaseFunction;
216
+ while (true) {
190
217
  try {
191
- (0, proper_lockfile_1.unlockSync)(extractedPath);
218
+ releaseFunction = (0, proper_lockfile_1.lockSync)(extractedPath, { realpath: false });
219
+ break;
192
220
  }
193
221
  catch (e) {
222
+ if (e.code === 'ELOCKED') {
223
+ logger.log('Waiting for lock for MySQL version', version);
224
+ await (0, FileLock_1.waitForLock)(extractedPath, options);
225
+ logger.log('Lock is gone for version', version);
226
+ //If the binary does not exist after lock has been released (like if the download fails and the binary got deleted as a result)
227
+ //then the lock acquisition process should start again
228
+ const binaryExists = fs.existsSync(binaryPath);
229
+ if (!binaryExists)
230
+ continue;
231
+ return resolve(binaryPath);
232
+ }
194
233
  return reject(e);
195
234
  }
196
- return resolve(binaryPath);
197
235
  }
198
- catch (e) {
199
- if (String(e).includes('Lock file is already being held')) {
200
- logger.log('Waiting for lock for MySQL version', version);
201
- await (0, FileLock_1.waitForLock)(extractedPath, options);
202
- logger.log('Lock is gone for version', version);
203
- return resolve(binaryPath);
236
+ //The code below only runs if the lock has been acquired by us
237
+ let downloadTries = 0;
238
+ do {
239
+ try {
240
+ downloadTries++;
241
+ await downloadFromCDN(url, archivePath, logger);
242
+ await extractBinary(url, archivePath, extractedPath, logger);
243
+ break;
204
244
  }
205
- if (lockedByUs) {
245
+ catch (e) {
246
+ //Delete generated files since either download or extraction failed
206
247
  try {
207
248
  await Promise.all([
208
249
  fsPromises.rm(extractedPath, { force: true, recursive: true }),
209
250
  fsPromises.rm(archivePath, { force: true, recursive: true })
210
251
  ]);
211
252
  }
212
- finally {
253
+ catch (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
213
258
  try {
214
- (0, proper_lockfile_1.unlockSync)(extractedPath, { realpath: false });
259
+ releaseFunction();
215
260
  }
216
261
  catch (e) {
217
- logger.error('An error occurred while unlocking path:', e);
262
+ logger.error('An error occurred while releasing lock after downloadRetries exhaustion. The error was:', e);
218
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);
219
269
  }
220
270
  }
221
- return reject(e);
271
+ } while (downloadTries < options.downloadRetries);
272
+ try {
273
+ releaseFunction();
222
274
  }
275
+ catch (e) {
276
+ logger.error('An error occurred while releasing lock after successful binary download. The error was:', e);
277
+ }
278
+ return resolve(binaryPath);
223
279
  }
224
- const uuid = (0, crypto_1.randomUUID)();
225
- const zipFilepath = `${dirpath}/${uuid}.${fileExtension}`;
226
- logger.log('Binary filepath:', zipFilepath);
227
- const extractedPath = `${dirpath}/${uuid}`;
228
- try {
229
- await downloadFromCDN(url, zipFilepath, logger);
230
- }
231
- catch (e) {
232
- reject(e);
233
- }
234
- try {
235
- const binaryPath = await extractBinary(url, zipFilepath, extractedPath, logger);
236
- resolve(binaryPath);
237
- }
238
- catch (e) {
239
- 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);
240
313
  }
241
314
  });
242
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_execute, _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') {
@@ -83,7 +60,7 @@ class Executor {
83
60
  const versions = [];
84
61
  for (const dir of servers) {
85
62
  const path = `${process.env.PROGRAMFILES}\\MySQL\\${dir}\\bin\\mysqld`;
86
- const { error, stdout, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_execute).call(this, `"${path}" --version`);
63
+ const { error, stdout, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, path, ['--version']);
87
64
  if (error || stderr) {
88
65
  return reject(error || stderr);
89
66
  }
@@ -110,8 +87,8 @@ class Executor {
110
87
  }
111
88
  }
112
89
  else {
113
- const { error, stdout, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_execute).call(this, 'mysqld --version');
114
- if (stderr && stderr.includes('not found')) {
90
+ const { error, stdout, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, 'mysqld', ['--version']);
91
+ if (error && error.code === 'ENOENT') {
115
92
  resolve(null);
116
93
  }
117
94
  else if (error || stderr) {
@@ -161,22 +138,16 @@ class Executor {
161
138
  } while (retries <= options.portRetries);
162
139
  }
163
140
  }
164
- _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_execute(command) {
141
+ _Executor_instances = new WeakSet(), _Executor_executeFile = function _Executor_executeFile(command, args) {
165
142
  return new Promise(resolve => {
166
- (0, child_process_1.exec)(command, { signal: AbortSignal_1.default.signal }, (error, stdout, stderr) => {
143
+ (0, child_process_1.execFile)(command, args, { signal: AbortSignal_1.default.signal }, (error, stdout, stderr) => {
167
144
  resolve({ error, stdout, stderr });
168
145
  });
169
146
  });
170
- }, _Executor_executeFile = function _Executor_executeFile(command, args, cwd) {
171
- return new Promise(resolve => {
172
- (0, child_process_1.execFile)(command, args, { signal: AbortSignal_1.default.signal, cwd }, (error, stdout, stderr) => {
173
- resolve({ stdout, stderr: (error === null || error === void 0 ? void 0 : error.message) || stderr });
174
- });
175
- });
176
147
  }, _Executor_killProcess = async function _Executor_killProcess(process) {
177
148
  let killed = false;
178
149
  if (os.platform() === 'win32') {
179
- const { error, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_execute).call(this, `taskkill /pid ${process.pid} /t /f`);
150
+ const { error, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, 'taskkill', ['/pid', String(process.pid), '/t', '/f']);
180
151
  if (!error && !stderr) {
181
152
  killed = true;
182
153
  }
@@ -188,6 +159,28 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
188
159
  killed = process.kill();
189
160
  }
190
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
+ }
191
184
  }, _Executor_startMySQLProcess = function _Executor_startMySQLProcess(options, port, mySQLXPort, datadir, dbPath, binaryFilepath) {
192
185
  const errors = [];
193
186
  const logFile = `${dbPath}/log.log`;
@@ -213,7 +206,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
213
206
  if (portIssue || xPortIssue) {
214
207
  this.logger.log('Error log when exiting for port in use error:', errorLog);
215
208
  try {
216
- await this.deleteDatabaseDirectory(options.dbPath);
209
+ await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_deleteDatabaseDirectory).call(this, options.dbPath);
217
210
  }
218
211
  catch (e) {
219
212
  this.logger.error(e);
@@ -223,7 +216,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
223
216
  }
224
217
  try {
225
218
  if (options.deleteDBAfterStopped) {
226
- await this.deleteDatabaseDirectory(dbPath);
219
+ await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_deleteDatabaseDirectory).call(this, dbPath);
227
220
  }
228
221
  }
229
222
  catch (e) {
@@ -310,7 +303,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
310
303
  await fsPromises.mkdir(datadir, { recursive: true });
311
304
  let stderr;
312
305
  if (binaryFilepath === 'mysqld') {
313
- const { error, stderr: output } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_execute).call(this, `mysqld --no-defaults --datadir=${datadir} --initialize-insecure`);
306
+ const { error, stderr: output } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, 'mysqld', ['--no-defaults', `--datadir=${datadir}`, '--initialize-insecure']);
314
307
  stderr = output;
315
308
  if (error) {
316
309
  this.logger.error('An error occurred while initializing database with system-installed MySQL:', error);
@@ -320,7 +313,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
320
313
  else {
321
314
  let result;
322
315
  try {
323
- result = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, `${binaryFilepath}`, [`--no-defaults`, `--datadir=${datadir}`, `--initialize-insecure`], (0, path_1.resolve)(`${binaryFilepath}/..`));
316
+ result = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, `${binaryFilepath}`, [`--no-defaults`, `--datadir=${datadir}`, `--initialize-insecure`]);
324
317
  }
325
318
  catch (e) {
326
319
  this.logger.error('Error occurred from executeFile:', e);
@@ -342,7 +335,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
342
335
  throw 'Tried to copy libaio into lib folder and MySQL is still failing to initialize. Please check the console for more information.';
343
336
  }
344
337
  if (binaryFilepath.slice(-16) === 'mysql/bin/mysqld') {
345
- const { error: lderror, stdout, stderr: ldstderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_execute).call(this, 'ldconfig -p');
338
+ const { error: lderror, stdout, stderr: ldstderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, 'ldconfig', ['-p']);
346
339
  if (lderror || ldstderr) {
347
340
  this.logger.error('The following libaio error occurred:', stderr);
348
341
  this.logger.error('After the libaio error, an ldconfig error occurred:', lderror || ldstderr);
@@ -358,8 +351,34 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
358
351
  const libaioSymlinkPath = libaioEntry.slice(libaioPathIndex + 3);
359
352
  const libaioPath = await fsPromises.realpath(libaioSymlinkPath);
360
353
  const copyPath = (0, path_1.resolve)(`${binaryFilepath}/../../lib/private/libaio.so.1`);
361
- try {
362
- (0, proper_lockfile_1.lockSync)(copyPath, { realpath: false });
354
+ let lockRelease;
355
+ while (true) {
356
+ try {
357
+ lockRelease = (0, proper_lockfile_1.lockSync)(copyPath, { realpath: false });
358
+ break;
359
+ }
360
+ catch (e) {
361
+ if (e.code === 'ELOCKED') {
362
+ this.logger.log('Waiting for lock for libaio copy');
363
+ await (0, FileLock_1.waitForLock)(copyPath, options);
364
+ this.logger.log('Lock is gone for libaio copy');
365
+ //If libaio does not exist after the lock has been released (like if the copy fails)
366
+ //then the lock acquisition process should start again
367
+ const binaryExists = fs.existsSync(copyPath);
368
+ if (!binaryExists)
369
+ continue;
370
+ break;
371
+ }
372
+ else {
373
+ this.logger.error('An error occurred from locking libaio section:', e);
374
+ throw e;
375
+ }
376
+ }
377
+ }
378
+ if (lockRelease) {
379
+ //This code will only run if the lock has been acquired successfully.
380
+ //If the lock was already acquired by some other process, this process would have already waited for the lock, so no copying needs to be done since it's already happened.
381
+ //If the lock failed to acquire for some other reason, the error would've already been thrown.
363
382
  this.logger.log('libaio copy path:', copyPath, '| libaio symlink path:', libaioSymlinkPath, '| libaio actual path:', libaioPath);
364
383
  let copyError;
365
384
  try {
@@ -377,7 +396,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
377
396
  }
378
397
  finally {
379
398
  try {
380
- (0, proper_lockfile_1.unlockSync)(copyPath, { realpath: false });
399
+ lockRelease();
381
400
  }
382
401
  catch (e) {
383
402
  this.logger.error('Error unlocking libaio file:', e);
@@ -387,20 +406,11 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
387
406
  }
388
407
  //Retry setting up directory now that libaio has been copied
389
408
  this.logger.log('Retrying directory setup');
390
- await this.deleteDatabaseDirectory(datadir);
409
+ await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_deleteDatabaseDirectory).call(this, datadir);
391
410
  await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_setupDataDirectories).call(this, options, binaryFilepath, datadir, false);
392
411
  return;
393
412
  }
394
413
  }
395
- catch (error) {
396
- if (String(error).includes('Lock file is already being held')) {
397
- this.logger.log('Waiting for lock for libaio copy');
398
- await (0, FileLock_1.waitForLock)(copyPath, options);
399
- this.logger.log('Lock is gone for libaio copy');
400
- }
401
- this.logger.error('An error occurred from locking libaio section:', error);
402
- throw error;
403
- }
404
414
  }
405
415
  else {
406
416
  throw 'Cannot recognize file structure for the MySQL binary folder. This was caused by not being able to find libaio. Try installing libaio. Learn more at https://dev.mysql.com/doc/refman/en/binary-installation.html';
@@ -8,7 +8,7 @@ function waitForLock(path, options) {
8
8
  while (retries <= options.lockRetries) {
9
9
  retries++;
10
10
  try {
11
- const locked = (0, proper_lockfile_1.checkSync)(path);
11
+ const locked = (0, proper_lockfile_1.checkSync)(path, { realpath: false });
12
12
  if (!locked) {
13
13
  return resolve();
14
14
  }
@@ -11,17 +11,17 @@ class Logger {
11
11
  }
12
12
  log(...args) {
13
13
  if (this.LOG_LEVEL === 0) {
14
- console.log(...args);
14
+ console.log('[mysql-memory-server - LOG]:', ...args);
15
15
  }
16
16
  }
17
17
  warn(...args) {
18
18
  if (this.LOG_LEVEL <= 1) {
19
- console.warn(...args);
19
+ console.warn('[mysql-memory-server - WARN]:', ...args);
20
20
  }
21
21
  }
22
22
  error(...args) {
23
23
  if (this.LOG_LEVEL <= 2) {
24
- console.error(...args);
24
+ console.error('[mysql-memory-server - ERROR]:', ...args);
25
25
  }
26
26
  }
27
27
  }
@@ -1,4 +1,4 @@
1
- import { ExecException } from "child_process";
1
+ import { ExecFileException } from "child_process";
2
2
  export type LOG_LEVEL = 'LOG' | 'WARN' | 'ERROR';
3
3
  export type ServerOptions = {
4
4
  version?: string;
@@ -14,6 +14,8 @@ export type ServerOptions = {
14
14
  ignoreUnsupportedSystemVersion?: boolean;
15
15
  port?: number;
16
16
  xPort?: number;
17
+ binaryDirectoryPath?: string;
18
+ downloadRetries?: number;
17
19
  };
18
20
  export type InternalServerOptions = {
19
21
  version?: string;
@@ -29,12 +31,14 @@ export type InternalServerOptions = {
29
31
  ignoreUnsupportedSystemVersion: boolean;
30
32
  port: number;
31
33
  xPort: number;
34
+ binaryDirectoryPath: string;
35
+ downloadRetries: number;
32
36
  };
33
37
  export type ExecutorOptions = {
34
38
  logLevel: LOG_LEVEL;
35
39
  };
36
- export type ExecuteReturn = {
37
- error: ExecException | null;
40
+ export type ExecuteFileReturn = {
41
+ error: ExecFileException | null;
38
42
  stdout: string;
39
43
  stderr: string;
40
44
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mysql-memory-server",
3
- "version": "1.4.2",
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",
@@ -20,20 +20,24 @@
20
20
  "test:ci": "jest --testPathIgnorePatterns=/stress-tests/ --setupFilesAfterEnv ./ciSetup.js --verbose",
21
21
  "stress": "jest --runTestsByPath stress-tests/stress.test.ts --setupFilesAfterEnv ./ciSetup.js --verbose"
22
22
  },
23
+ "engines": {
24
+ "node": ">=16.6.0",
25
+ "bun": ">=1.0.0"
26
+ },
23
27
  "author": "Sebastian-Webster",
24
28
  "license": "MIT",
25
29
  "devDependencies": {
26
- "@babel/core": "^7.24.9",
27
- "@babel/preset-env": "^7.25.4",
28
- "@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",
29
33
  "@types/adm-zip": "^0.5.5",
30
- "@types/node": "^22.5.4",
34
+ "@types/node": "^22.7.5",
31
35
  "@types/proper-lockfile": "^4.1.4",
32
36
  "@types/semver": "^7.5.8",
33
37
  "babel-jest": "^29.7.0",
34
38
  "jest": "^29.7.0",
35
- "mysql2": "^3.10.3",
36
- "typescript": "^5.5.4"
39
+ "mysql2": "^3.11.3",
40
+ "typescript": "^5.6.3"
37
41
  },
38
42
  "dependencies": {
39
43
  "adm-zip": "^0.5.16",