atomically 1.3.2 → 1.7.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/dist/consts.d.ts CHANGED
@@ -8,5 +8,6 @@ declare const DEFAULT_TIMEOUT_SYNC = 100;
8
8
  declare const IS_POSIX = true;
9
9
  declare const IS_USER_ROOT: boolean;
10
10
  declare const LIMIT_BASENAME_LENGTH = 128;
11
+ declare const LIMIT_FILES_DESCRIPTORS = 10000;
11
12
  declare const NOOP: () => void;
12
- export { DEFAULT_ENCODING, DEFAULT_FILE_MODE, DEFAULT_FOLDER_MODE, DEFAULT_READ_OPTIONS, DEFAULT_WRITE_OPTIONS, DEFAULT_TIMEOUT_ASYNC, DEFAULT_TIMEOUT_SYNC, IS_POSIX, IS_USER_ROOT, LIMIT_BASENAME_LENGTH, NOOP };
13
+ export { DEFAULT_ENCODING, DEFAULT_FILE_MODE, DEFAULT_FOLDER_MODE, DEFAULT_READ_OPTIONS, DEFAULT_WRITE_OPTIONS, DEFAULT_TIMEOUT_ASYNC, DEFAULT_TIMEOUT_SYNC, IS_POSIX, IS_USER_ROOT, LIMIT_BASENAME_LENGTH, LIMIT_FILES_DESCRIPTORS, NOOP };
package/dist/consts.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /* CONSTS */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.NOOP = exports.LIMIT_BASENAME_LENGTH = exports.IS_USER_ROOT = exports.IS_POSIX = exports.DEFAULT_TIMEOUT_SYNC = exports.DEFAULT_TIMEOUT_ASYNC = exports.DEFAULT_WRITE_OPTIONS = exports.DEFAULT_READ_OPTIONS = exports.DEFAULT_FOLDER_MODE = exports.DEFAULT_FILE_MODE = exports.DEFAULT_ENCODING = void 0;
4
+ exports.NOOP = exports.LIMIT_FILES_DESCRIPTORS = exports.LIMIT_BASENAME_LENGTH = exports.IS_USER_ROOT = exports.IS_POSIX = exports.DEFAULT_TIMEOUT_SYNC = exports.DEFAULT_TIMEOUT_ASYNC = exports.DEFAULT_WRITE_OPTIONS = exports.DEFAULT_READ_OPTIONS = exports.DEFAULT_FOLDER_MODE = exports.DEFAULT_FILE_MODE = exports.DEFAULT_ENCODING = void 0;
5
5
  const DEFAULT_ENCODING = 'utf8';
6
6
  exports.DEFAULT_ENCODING = DEFAULT_ENCODING;
7
7
  const DEFAULT_FILE_MODE = 0o666;
@@ -22,5 +22,7 @@ const IS_USER_ROOT = process.getuid ? !process.getuid() : false;
22
22
  exports.IS_USER_ROOT = IS_USER_ROOT;
23
23
  const LIMIT_BASENAME_LENGTH = 128; //TODO: fetch the real limit from the filesystem //TODO: fetch the whole-path length limit too
24
24
  exports.LIMIT_BASENAME_LENGTH = LIMIT_BASENAME_LENGTH;
25
+ const LIMIT_FILES_DESCRIPTORS = 10000; //TODO: fetch the real limit from the filesystem
26
+ exports.LIMIT_FILES_DESCRIPTORS = LIMIT_FILES_DESCRIPTORS;
25
27
  const NOOP = () => { };
26
28
  exports.NOOP = NOOP;
@@ -14,6 +14,7 @@ declare const FS: {
14
14
  openRetry: import("../types").FN<[number], typeof fs.open.__promisify__>;
15
15
  readFileRetry: import("../types").FN<[number], typeof fs.readFile.__promisify__>;
16
16
  renameRetry: import("../types").FN<[number], typeof fs.rename.__promisify__>;
17
+ statRetry: import("../types").FN<[number], typeof fs.stat.__promisify__>;
17
18
  writeRetry: import("../types").FN<[number], typeof fs.write.__promisify__>;
18
19
  chmodSyncAttempt: typeof fs.chmodSync;
19
20
  chownSyncAttempt: typeof fs.chownSync;
@@ -27,6 +28,7 @@ declare const FS: {
27
28
  openSyncRetry: import("../types").FN<[number], typeof fs.openSync>;
28
29
  readFileSyncRetry: import("../types").FN<[number], typeof fs.readFileSync>;
29
30
  renameSyncRetry: import("../types").FN<[number], typeof fs.renameSync>;
31
+ statSyncRetry: import("../types").FN<[number], typeof fs.statSync>;
30
32
  writeSyncRetry: import("../types").FN<[number], typeof fs.writeSync>;
31
33
  };
32
34
  export default FS;
package/dist/utils/fs.js CHANGED
@@ -21,6 +21,7 @@ const FS = {
21
21
  openRetry: retryify_1.retryifyAsync(util_1.promisify(fs.open), fs_handlers_1.default.isRetriableError),
22
22
  readFileRetry: retryify_1.retryifyAsync(util_1.promisify(fs.readFile), fs_handlers_1.default.isRetriableError),
23
23
  renameRetry: retryify_1.retryifyAsync(util_1.promisify(fs.rename), fs_handlers_1.default.isRetriableError),
24
+ statRetry: retryify_1.retryifyAsync(util_1.promisify(fs.stat), fs_handlers_1.default.isRetriableError),
24
25
  writeRetry: retryify_1.retryifyAsync(util_1.promisify(fs.write), fs_handlers_1.default.isRetriableError),
25
26
  chmodSyncAttempt: attemptify_1.attemptifySync(fs.chmodSync, fs_handlers_1.default.onChangeError),
26
27
  chownSyncAttempt: attemptify_1.attemptifySync(fs.chownSync, fs_handlers_1.default.onChangeError),
@@ -34,6 +35,7 @@ const FS = {
34
35
  openSyncRetry: retryify_1.retryifySync(fs.openSync, fs_handlers_1.default.isRetriableError),
35
36
  readFileSyncRetry: retryify_1.retryifySync(fs.readFileSync, fs_handlers_1.default.isRetriableError),
36
37
  renameSyncRetry: retryify_1.retryifySync(fs.renameSync, fs_handlers_1.default.isRetriableError),
38
+ statSyncRetry: retryify_1.retryifySync(fs.statSync, fs_handlers_1.default.isRetriableError),
37
39
  writeSyncRetry: retryify_1.retryifySync(fs.writeSync, fs_handlers_1.default.isRetriableError)
38
40
  };
39
41
  /* EXPORT */
@@ -2,18 +2,25 @@
2
2
  /* IMPORT */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.retryifySync = exports.retryifyAsync = void 0;
5
+ const retryify_queue_1 = require("./retryify_queue");
5
6
  /* RETRYIFY */
6
- //TODO: Maybe publish this as a standalone package
7
- //TODO: Implement a more sophisticaed, less intensive, retry strategy
8
7
  const retryifyAsync = (fn, isRetriableError) => {
9
8
  return function (timestamp) {
10
9
  return function attempt() {
11
- return fn.apply(undefined, arguments).catch(error => {
12
- if (Date.now() > timestamp)
10
+ return retryify_queue_1.default.schedule().then(cleanup => {
11
+ return fn.apply(undefined, arguments).then(result => {
12
+ cleanup();
13
+ return result;
14
+ }, error => {
15
+ cleanup();
16
+ if (Date.now() >= timestamp)
17
+ throw error;
18
+ if (isRetriableError(error)) {
19
+ const delay = Math.round(100 + (400 * Math.random())), delayPromise = new Promise(resolve => setTimeout(resolve, delay));
20
+ return delayPromise.then(() => attempt.apply(undefined, arguments));
21
+ }
13
22
  throw error;
14
- if (isRetriableError(error))
15
- return attempt.apply(undefined, arguments);
16
- throw error;
23
+ });
17
24
  });
18
25
  };
19
26
  };
@@ -0,0 +1,15 @@
1
+ /// <reference types="node" />
2
+ declare const RetryfyQueue: {
3
+ interval: number;
4
+ intervalId: NodeJS.Timeout | undefined;
5
+ limit: number;
6
+ queueActive: Set<Function>;
7
+ queueWaiting: Set<Function>;
8
+ init: () => void;
9
+ reset: () => void;
10
+ add: (fn: Function) => void;
11
+ remove: (fn: Function) => void;
12
+ schedule: () => Promise<Function>;
13
+ tick: () => void;
14
+ };
15
+ export default RetryfyQueue;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /* IMPORT */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const consts_1 = require("../consts");
5
+ /* RETRYIFY QUEUE */
6
+ const RetryfyQueue = {
7
+ interval: 25,
8
+ intervalId: undefined,
9
+ limit: consts_1.LIMIT_FILES_DESCRIPTORS,
10
+ queueActive: new Set(),
11
+ queueWaiting: new Set(),
12
+ init: () => {
13
+ if (RetryfyQueue.intervalId)
14
+ return;
15
+ RetryfyQueue.intervalId = setInterval(RetryfyQueue.tick, RetryfyQueue.interval);
16
+ },
17
+ reset: () => {
18
+ if (!RetryfyQueue.intervalId)
19
+ return;
20
+ clearInterval(RetryfyQueue.intervalId);
21
+ delete RetryfyQueue.intervalId;
22
+ },
23
+ add: (fn) => {
24
+ RetryfyQueue.queueWaiting.add(fn);
25
+ if (RetryfyQueue.queueActive.size < (RetryfyQueue.limit / 2)) { // Active queue not under preassure, executing immediately
26
+ RetryfyQueue.tick();
27
+ }
28
+ else {
29
+ RetryfyQueue.init();
30
+ }
31
+ },
32
+ remove: (fn) => {
33
+ RetryfyQueue.queueWaiting.delete(fn);
34
+ RetryfyQueue.queueActive.delete(fn);
35
+ },
36
+ schedule: () => {
37
+ return new Promise(resolve => {
38
+ const cleanup = () => RetryfyQueue.remove(resolver);
39
+ const resolver = () => resolve(cleanup);
40
+ RetryfyQueue.add(resolver);
41
+ });
42
+ },
43
+ tick: () => {
44
+ if (RetryfyQueue.queueActive.size >= RetryfyQueue.limit)
45
+ return;
46
+ if (!RetryfyQueue.queueWaiting.size)
47
+ return RetryfyQueue.reset();
48
+ for (const fn of RetryfyQueue.queueWaiting) {
49
+ if (RetryfyQueue.queueActive.size >= RetryfyQueue.limit)
50
+ break;
51
+ RetryfyQueue.queueWaiting.delete(fn);
52
+ RetryfyQueue.queueActive.add(fn);
53
+ fn();
54
+ }
55
+ }
56
+ };
57
+ /* EXPORT */
58
+ exports.default = RetryfyQueue;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "atomically",
3
3
  "description": "Read and write files atomically and reliably.",
4
- "version": "1.3.2",
4
+ "version": "1.7.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
package/src/consts.ts CHANGED
@@ -21,8 +21,10 @@ const IS_USER_ROOT = process.getuid ? !process.getuid () : false;
21
21
 
22
22
  const LIMIT_BASENAME_LENGTH = 128; //TODO: fetch the real limit from the filesystem //TODO: fetch the whole-path length limit too
23
23
 
24
+ const LIMIT_FILES_DESCRIPTORS = 10000; //TODO: fetch the real limit from the filesystem
25
+
24
26
  const NOOP = () => {};
25
27
 
26
28
  /* EXPORT */
27
29
 
28
- export {DEFAULT_ENCODING, DEFAULT_FILE_MODE, DEFAULT_FOLDER_MODE, DEFAULT_READ_OPTIONS, DEFAULT_WRITE_OPTIONS, DEFAULT_TIMEOUT_ASYNC, DEFAULT_TIMEOUT_SYNC, IS_POSIX, IS_USER_ROOT, LIMIT_BASENAME_LENGTH, NOOP};
30
+ export {DEFAULT_ENCODING, DEFAULT_FILE_MODE, DEFAULT_FOLDER_MODE, DEFAULT_READ_OPTIONS, DEFAULT_WRITE_OPTIONS, DEFAULT_TIMEOUT_ASYNC, DEFAULT_TIMEOUT_SYNC, IS_POSIX, IS_USER_ROOT, LIMIT_BASENAME_LENGTH, LIMIT_FILES_DESCRIPTORS, NOOP};
package/src/utils/fs.ts CHANGED
@@ -25,6 +25,7 @@ const FS = {
25
25
  openRetry: retryifyAsync ( promisify ( fs.open ), Handlers.isRetriableError ),
26
26
  readFileRetry: retryifyAsync ( promisify ( fs.readFile ), Handlers.isRetriableError ),
27
27
  renameRetry: retryifyAsync ( promisify ( fs.rename ), Handlers.isRetriableError ),
28
+ statRetry: retryifyAsync ( promisify ( fs.stat ), Handlers.isRetriableError ),
28
29
  writeRetry: retryifyAsync ( promisify ( fs.write ), Handlers.isRetriableError ),
29
30
 
30
31
  chmodSyncAttempt: attemptifySync ( fs.chmodSync, Handlers.onChangeError ),
@@ -40,6 +41,7 @@ const FS = {
40
41
  openSyncRetry: retryifySync ( fs.openSync, Handlers.isRetriableError ),
41
42
  readFileSyncRetry: retryifySync ( fs.readFileSync, Handlers.isRetriableError ),
42
43
  renameSyncRetry: retryifySync ( fs.renameSync, Handlers.isRetriableError ),
44
+ statSyncRetry: retryifySync ( fs.statSync, Handlers.isRetriableError ),
43
45
  writeSyncRetry: retryifySync ( fs.writeSync, Handlers.isRetriableError )
44
46
 
45
47
  };
@@ -2,25 +2,42 @@
2
2
  /* IMPORT */
3
3
 
4
4
  import {Exception, FN} from '../types';
5
+ import RetryfyQueue from './retryify_queue';
5
6
 
6
7
  /* RETRYIFY */
7
8
 
8
- //TODO: Maybe publish this as a standalone package
9
- //TODO: Implement a more sophisticaed, less intensive, retry strategy
10
-
11
9
  const retryifyAsync = <T extends FN> ( fn: T, isRetriableError: FN<[Exception], boolean | void> ): FN<[number], T> => {
12
10
 
13
11
  return function ( timestamp: number ) {
14
12
 
15
13
  return function attempt () {
16
14
 
17
- return fn.apply ( undefined, arguments ).catch ( error => {
15
+ return RetryfyQueue.schedule ().then ( cleanup => {
18
16
 
19
- if ( Date.now () > timestamp ) throw error;
17
+ return fn.apply ( undefined, arguments ).then ( result => {
20
18
 
21
- if ( isRetriableError ( error ) ) return attempt.apply ( undefined, arguments );
19
+ cleanup ();
22
20
 
23
- throw error;
21
+ return result;
22
+
23
+ }, error => {
24
+
25
+ cleanup ();
26
+
27
+ if ( Date.now () >= timestamp ) throw error;
28
+
29
+ if ( isRetriableError ( error ) ) {
30
+
31
+ const delay = Math.round ( 100 + ( 400 * Math.random () ) ),
32
+ delayPromise = new Promise ( resolve => setTimeout ( resolve, delay ) );
33
+
34
+ return delayPromise.then ( () => attempt.apply ( undefined, arguments ) );
35
+
36
+ }
37
+
38
+ throw error;
39
+
40
+ });
24
41
 
25
42
  });
26
43
 
@@ -0,0 +1,95 @@
1
+
2
+ /* IMPORT */
3
+
4
+ import {LIMIT_FILES_DESCRIPTORS} from '../consts';
5
+
6
+ /* RETRYIFY QUEUE */
7
+
8
+ const RetryfyQueue = {
9
+
10
+ interval: 25,
11
+ intervalId: <NodeJS.Timeout | undefined> undefined,
12
+ limit: LIMIT_FILES_DESCRIPTORS,
13
+ queueActive: new Set<Function> (),
14
+ queueWaiting: new Set<Function> (),
15
+
16
+ init: (): void => {
17
+
18
+ if ( RetryfyQueue.intervalId ) return;
19
+
20
+ RetryfyQueue.intervalId = setInterval ( RetryfyQueue.tick, RetryfyQueue.interval );
21
+
22
+ },
23
+
24
+ reset: (): void => {
25
+
26
+ if ( !RetryfyQueue.intervalId ) return;
27
+
28
+ clearInterval ( RetryfyQueue.intervalId );
29
+
30
+ delete RetryfyQueue.intervalId;
31
+
32
+ },
33
+
34
+ add: ( fn: Function ): void => {
35
+
36
+ RetryfyQueue.queueWaiting.add ( fn );
37
+
38
+ if ( RetryfyQueue.queueActive.size < ( RetryfyQueue.limit / 2 ) ) { // Active queue not under preassure, executing immediately
39
+
40
+ RetryfyQueue.tick ();
41
+
42
+ } else {
43
+
44
+ RetryfyQueue.init ();
45
+
46
+ }
47
+
48
+ },
49
+
50
+ remove: ( fn: Function ): void => {
51
+
52
+ RetryfyQueue.queueWaiting.delete ( fn );
53
+
54
+ RetryfyQueue.queueActive.delete ( fn );
55
+
56
+ },
57
+
58
+ schedule: (): Promise<Function> => {
59
+
60
+ return new Promise ( resolve => {
61
+
62
+ const cleanup = () => RetryfyQueue.remove ( resolver );
63
+
64
+ const resolver = () => resolve ( cleanup );
65
+
66
+ RetryfyQueue.add ( resolver );
67
+
68
+ });
69
+
70
+ },
71
+
72
+ tick: (): void => {
73
+
74
+ if ( RetryfyQueue.queueActive.size >= RetryfyQueue.limit ) return;
75
+
76
+ if ( !RetryfyQueue.queueWaiting.size ) return RetryfyQueue.reset ();
77
+
78
+ for ( const fn of RetryfyQueue.queueWaiting ) {
79
+
80
+ if ( RetryfyQueue.queueActive.size >= RetryfyQueue.limit ) break;
81
+
82
+ RetryfyQueue.queueWaiting.delete ( fn );
83
+ RetryfyQueue.queueActive.add ( fn );
84
+
85
+ fn ();
86
+
87
+ }
88
+
89
+ }
90
+
91
+ };
92
+
93
+ /* EXPORT */
94
+
95
+ export default RetryfyQueue;
package/test/basic.js CHANGED
@@ -116,7 +116,7 @@ const fsMock = Object.assign ( {}, fs, {
116
116
 
117
117
  const makeUnstableAsyncFn = function () {
118
118
  return function () {
119
- if ( Math.random () <= .9 ) {
119
+ if ( Math.random () <= .75 ) {
120
120
  const code = _.shuffle ([ 'EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY', 'EACCESS', 'EPERM' ])[0];
121
121
  throw createErr ( code );
122
122
  }
@@ -126,7 +126,7 @@ const makeUnstableAsyncFn = function () {
126
126
 
127
127
  const makeUnstableSyncFn = function ( fn ) {
128
128
  return function () {
129
- if ( Math.random () <= .9 ) {
129
+ if ( Math.random () <= .75 ) {
130
130
  const code = _.shuffle ([ 'EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY', 'EACCESS', 'EPERM' ])[0];
131
131
  throw createErr ( code );
132
132
  }