Repository: char0n/ramda-adjunct
Pull request #905 (2019-05-03 20:48:35)

allSettledP
https://github.com/char0n/ramda-adjunct/issues/761


Patch:
diff --git a/src/allP.js b/src/allP.js
index fdef7c324d..e5747facb5 100644
--- a/src/allP.js
+++ b/src/allP.js
@@ -12,7 +12,7 @@ import curry1 from 'ramda/src/internal/_curry1';
  * @memberOf RA
  * @since {@link https://char0n.github.io/ramda-adjunct/2.3.0|v2.3.0}
  * @category Function
- * @sig [Promise a] -> [a]
+ * @sig [Promise a] -> Promise [a]
  * @param {Iterable.<*>} iterable An iterable object such as an Array or String
  * @return {Promise} An already resolved Promise if the iterable passed is empty. An asynchronously resolved Promise if the iterable passed contains no promises. Note, Google Chrome 58 returns an already resolved promise in this case. A pending Promise in all other cases. This returned promise is then resolved/rejected asynchronously (as soon as the stack is empty) when all the promises in the given iterable have resolved, or if any of the promises reject. See the example about "Asynchronicity or synchronicity of allP" below.
  * @see {@link RA.resolveP|resolveP}, {@link RA.rejectP|rejectP}
diff --git a/src/allSettledP.js b/src/allSettledP.js
new file mode 100644
index 0000000000..1a43e73e45
--- /dev/null
+++ b/src/allSettledP.js
@@ -0,0 +1,35 @@
+import { bind } from 'ramda';
+import curry1 from 'ramda/src/internal/_curry1';
+
+import isFunction from './isFunction';
+import polyfill from './internal/polyfills/Promise.allSettled';
+
+export const allSettledPPolyfill = curry1(polyfill);
+
+/**
+ * Returns a promise that is fulfilled with an array of promise state snapshots,
+ * but only after all the original promises have settled, i.e. become either fulfilled or rejected.
+ * We say that a promise is settled if it is not pending, i.e. if it is either fulfilled or rejected.
+ *
+ * @func allSettledP
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.18.0|v2.18.0}
+ * @category Function
+ * @typedef Settlement = { status: String, value: * }
+ * @sig [Promise a] -> Promise [Settlement a]
+ * @param {Iterable.<*>} iterable An iterable object such as an Array or String
+ * @return {Promise} Returns a promise that is fulfilled with an array of promise state snapshots
+ * @see {@link RA.allP|allP}
+ * @example
+ *
+ * RA.allSettledP([
+ *   Promise.resolve(1),
+ *   2,
+ *   Promise.reject(3),
+ * ]); //=> Promise([{ status: 'fulfilled', value: 1 }, { status: 'fulfilled', value: 2 }, { status: 'rejected', reason: 3 }])
+ */
+const allSettledP = isFunction(Promise.allSettled)
+  ? curry1(bind(Promise.allSettled, Promise))
+  : allSettledPPolyfill;
+
+export default allSettledP;
diff --git a/src/index.d.ts b/src/index.d.ts
index 5708015113..d2d7f78759 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -758,6 +758,13 @@ declare namespace RamdaAdjunct {
          */
         allP<T>(iterator: Iterator<T>): Promise<T[]>;
 
+       /**
+        * allSettledP returns a promise that is fulfilled with an array of promise state snapshots,
+        * but only after all the original promises have settled, i.e. become either fulfilled or rejected.
+        * We say that a promise is settled if it is not pending, i.e. if it is either fulfilled or rejected.
+        */
+        allSettledP<T>(iterator: Iterator<T>): Promise<T[]>;
+
         /**
          * Composable shortcut for `Promise.resolve`.
          *
diff --git a/src/index.js b/src/index.js
index d651e4352d..5885d13236 100644
--- a/src/index.js
+++ b/src/index.js
@@ -92,6 +92,7 @@ export { default as resolveP } from './resolveP';
 export { default as rejectP } from './rejectP';
 export { default as thenP } from './thenP';
 export { default as then } from './thenP';
+export { default as allSettledP } from './allSettledP';
 export { default as Y } from './Y';
 export { default as seq } from './seq';
 export { default as sequencing } from './seq';
diff --git a/src/internal/polyfills/Promise.allSettled.js b/src/internal/polyfills/Promise.allSettled.js
new file mode 100644
index 0000000000..3a4e83db72
--- /dev/null
+++ b/src/internal/polyfills/Promise.allSettled.js
@@ -0,0 +1,21 @@
+import { map } from 'ramda';
+
+import allP from '../../allP';
+import resolveP from '../../resolveP';
+
+const onFulfill = value => ({ status: 'fulfilled', value });
+const onReject = reason => ({ status: 'rejected', reason });
+
+const allSettledPolyfill = iterable => {
+  const array = map(
+    p =>
+      resolveP(p)
+        .then(onFulfill)
+        .catch(onReject),
+    [...iterable]
+  );
+
+  return allP(array);
+};
+
+export default allSettledPolyfill;


Test patch:
diff --git a/test/allSettledP.js b/test/allSettledP.js
new file mode 100644
index 0000000000..50c0ecdec3
--- /dev/null
+++ b/test/allSettledP.js
@@ -0,0 +1,225 @@
+import * as R from 'ramda';
+import { assert } from 'chai';
+
+import * as RA from '../src';
+import { allSettledPPolyfill } from '../src/allSettledP';
+
+describe('allSettledP', function() {
+  context('given list of fulfilled promises', function() {
+    specify('should return list of fulfilled values', async function() {
+      const p1 = RA.resolveP(1);
+      const p2 = RA.resolveP(2);
+      const p3 = RA.resolveP(3);
+      const actual = [p1, p2, p3];
+      const expected = [
+        { status: 'fulfilled', value: 1 },
+        { status: 'fulfilled', value: 2 },
+        { status: 'fulfilled', value: 3 },
+      ];
+
+      assert.deepEqual(await RA.allSettledP(actual), expected);
+    });
+  });
+
+  context('given list of rejected promises', function() {
+    specify('should return list of rejected reasons', async function() {
+      const p1 = RA.rejectP(1);
+      const p2 = RA.rejectP(2);
+      const p3 = RA.rejectP(3);
+      const actual = [p1, p2, p3];
+      const expected = [
+        { status: 'rejected', reason: 1 },
+        { status: 'rejected', reason: 2 },
+        { status: 'rejected', reason: 3 },
+      ];
+
+      assert.deepEqual(await RA.allSettledP(actual), expected);
+    });
+  });
+
+  context('given list of fulfilled and rejected promises', function() {
+    specify(
+      'should return list of rejected reasons and fulfilled values',
+      async function() {
+        const p1 = RA.rejectP(1);
+        const p2 = RA.resolveP(2);
+        const p3 = RA.rejectP(3);
+        const actual = [p1, p2, p3];
+        const expected = [
+          { status: 'rejected', reason: 1 },
+          { status: 'fulfilled', value: 2 },
+          { status: 'rejected', reason: 3 },
+        ];
+
+        assert.deepEqual(await RA.allSettledP(actual), expected);
+      }
+    );
+  });
+
+  context('given list of values', function() {
+    specify('should return list of fulfilled values', async function() {
+      const v1 = 1;
+      const v2 = 2;
+      const v3 = 3;
+      const actual = [v1, v2, v3];
+      const expected = [
+        { status: 'fulfilled', value: 1 },
+        { status: 'fulfilled', value: 2 },
+        { status: 'fulfilled', value: 3 },
+      ];
+
+      assert.deepEqual(await RA.allSettledP(actual), expected);
+    });
+  });
+
+  context('given list of values and promises', function() {
+    specify('should return list of fulfilled values', async function() {
+      const p1 = RA.resolveP(1);
+      const v2 = 2;
+      const p3 = RA.resolveP(3);
+      const actual = [p1, v2, p3];
+      const expected = [
+        { status: 'fulfilled', value: 1 },
+        { status: 'fulfilled', value: 2 },
+        { status: 'fulfilled', value: 3 },
+      ];
+
+      assert.deepEqual(await RA.allSettledP(actual), expected);
+    });
+  });
+
+  context('given large number of promises', function() {
+    specify('should handle them properly', async function() {
+      const list = R.range(1, 1000);
+      const actual = list.map(RA.resolveP);
+      const expected = list.map(value => ({ status: 'fulfilled', value }));
+
+      assert.deepEqual(await RA.allSettledP(actual), expected);
+    });
+  });
+
+  it('should support placeholder to specify "gaps"', async function() {
+    const allSettledP = RA.allSettledP(R.__);
+    const p1 = RA.resolveP(1);
+    const v2 = 2;
+    const p3 = RA.resolveP(3);
+    const actual = [p1, v2, p3];
+    const expected = [
+      { status: 'fulfilled', value: 1 },
+      { status: 'fulfilled', value: 2 },
+      { status: 'fulfilled', value: 3 },
+    ];
+
+    assert.deepEqual(await allSettledP(actual), expected);
+  });
+
+  context('allSettledPPolyfill', function() {
+    context('given list of fulfilled promises', function() {
+      specify('should return list of fulfilled values', async function() {
+        const p1 = RA.resolveP(1);
+        const p2 = RA.resolveP(2);
+        const p3 = RA.resolveP(3);
+        const actual = [p1, p2, p3];
+        const expected = [
+          { status: 'fulfilled', value: 1 },
+          { status: 'fulfilled', value: 2 },
+          { status: 'fulfilled', value: 3 },
+        ];
+
+        assert.deepEqual(await allSettledPPolyfill(actual), expected);
+      });
+    });
+
+    context('given list of rejected promises', function() {
+      specify('should return list of rejected reasons', async function() {
+        const p1 = RA.rejectP(1);
+        const p2 = RA.rejectP(2);
+        const p3 = RA.rejectP(3);
+        const actual = [p1, p2, p3];
+        const expected = [
+          { status: 'rejected', reason: 1 },
+          { status: 'rejected', reason: 2 },
+          { status: 'rejected', reason: 3 },
+        ];
+
+        assert.deepEqual(await allSettledPPolyfill(actual), expected);
+      });
+    });
+
+    context('given list of fulfilled and rejected promises', function() {
+      specify(
+        'should return list of rejected reasons and fulfilled values',
+        async function() {
+          const p1 = RA.rejectP(1);
+          const p2 = RA.resolveP(2);
+          const p3 = RA.rejectP(3);
+          const actual = [p1, p2, p3];
+          const expected = [
+            { status: 'rejected', reason: 1 },
+            { status: 'fulfilled', value: 2 },
+            { status: 'rejected', reason: 3 },
+          ];
+
+          assert.deepEqual(await allSettledPPolyfill(actual), expected);
+        }
+      );
+    });
+
+    context('given list of values', function() {
+      specify('should return list of fulfilled values', async function() {
+        const v1 = 1;
+        const v2 = 2;
+        const v3 = 3;
+        const actual = [v1, v2, v3];
+        const expected = [
+          { status: 'fulfilled', value: 1 },
+          { status: 'fulfilled', value: 2 },
+          { status: 'fulfilled', value: 3 },
+        ];
+
+        assert.deepEqual(await allSettledPPolyfill(actual), expected);
+      });
+    });
+
+    context('given list of values and promises', function() {
+      specify('should return list of fulfilled values', async function() {
+        const p1 = RA.resolveP(1);
+        const v2 = 2;
+        const p3 = RA.resolveP(3);
+        const actual = [p1, v2, p3];
+        const expected = [
+          { status: 'fulfilled', value: 1 },
+          { status: 'fulfilled', value: 2 },
+          { status: 'fulfilled', value: 3 },
+        ];
+
+        assert.deepEqual(await allSettledPPolyfill(actual), expected);
+      });
+    });
+
+    context('given large number of promises', function() {
+      specify('should handle them properly', async function() {
+        const list = R.range(1, 1000);
+        const actual = list.map(RA.resolveP);
+        const expected = list.map(value => ({ status: 'fulfilled', value }));
+
+        assert.deepEqual(await allSettledPPolyfill(actual), expected);
+      });
+    });
+
+    it('should support placeholder to specify "gaps"', async function() {
+      const allSettledP = allSettledPPolyfill(R.__);
+      const p1 = RA.resolveP(1);
+      const v2 = 2;
+      const p3 = RA.resolveP(3);
+      const actual = [p1, v2, p3];
+      const expected = [
+        { status: 'fulfilled', value: 1 },
+        { status: 'fulfilled', value: 2 },
+        { status: 'fulfilled', value: 3 },
+      ];
+
+      assert.deepEqual(await allSettledP(actual), expected);
+    });
+  });
+});
diff --git a/test/isNotEmpty.js b/test/isNotEmpty.js
index a8d42f8be8..0f8489e5c7 100644
--- a/test/isNotEmpty.js
+++ b/test/isNotEmpty.js
@@ -1,52 +1,100 @@
+import { assert } from 'chai';
+import * as R from 'ramda';
+
 import * as RA from '../src';
-import eq from './shared/eq';
 
 describe('isNotEmpty', function() {
-  it('returns true for null', function() {
-    eq(RA.isNotEmpty(null), true);
+  context('given null', function() {
+    specify('should return true', function() {
+      assert.isTrue(RA.isNotEmpty(null));
+    });
+  });
+
+  context('given undefined', function() {
+    specify('should return true', function() {
+      assert.isTrue(RA.isNotEmpty(undefined));
+    });
   });
 
-  it('returns true for undefined', function() {
-    eq(RA.isNotEmpty(undefined), true);
+  context('given string', function() {
+    context('and the string is empty', function() {
+      specify('should return false', function() {
+        assert.isFalse(RA.isNotEmpty(''));
+      });
+    });
+
+    context('and the string contains empty spaces', function() {
+      specify('should return true', function() {
+        assert.isTrue(RA.isNotEmpty(' '));
+      });
+    });
   });
 
-  it('returns false for empty string', function() {
-    eq(RA.isNotEmpty(''), false);
-    eq(RA.isNotEmpty(' '), true);
+  context('given array', function() {
+    context('and the array is empty', function() {
+      specify('should return false', function() {
+        assert.isFalse(RA.isNotEmpty([]));
+      });
+    });
+
+    context('and the array contains another empty array', function() {
+      specify('should return true', function() {
+        assert.isTrue(RA.isNotEmpty([[]]));
+      });
+    });
   });
 
-  it('returns false for empty array', function() {
-    eq(RA.isNotEmpty([]), false);
-    eq(RA.isNotEmpty([[]]), true);
+  context('given object', function() {
+    context('and the object is empty', function() {
+      specify('should return false', function() {
+        assert.isFalse(RA.isNotEmpty({}));
+      });
+    });
+
+    context('and the object contains property', function() {
+      specify('should return true', function() {
+        assert.isTrue(RA.isNotEmpty({ x: 0 }));
+      });
+    });
   });
 
-  it('returns false for empty object', function() {
-    eq(RA.isNotEmpty({}), false);
-    eq(RA.isNotEmpty({ x: 0 }), true);
+  context('given arguments object', function() {
+    context('and arguments objects is empty', function() {
+      specify('should return false', function() {
+        assert.isFalse(
+          RA.isNotEmpty(
+            (function() {
+              return arguments;
+            })()
+          )
+        );
+      });
+    });
+
+    context('and arguments object is not empty', function() {
+      specify('should return true', function() {
+        assert.isTrue(
+          RA.isNotEmpty(
+            (function() {
+              return arguments;
+            })(0)
+          )
+        );
+      });
+    });
   });
 
-  it('returns false for empty arguments object', function() {
-    eq(
-      RA.isNotEmpty(
-        (function() {
-          return arguments;
-        })()
-      ),
-      false
-    );
-    eq(
-      RA.isNotEmpty(
-        (function() {
-          return arguments;
-        })(0)
-      ),
-      true
-    );
+  context('given some non empty values', function() {
+    specify('should return true', function() {
+      assert.isTrue(RA.isNotEmpty(0));
+      assert.isTrue(RA.isNotEmpty(NaN));
+      assert.isTrue(RA.isNotEmpty(['']));
+    });
   });
 
-  it('returns true for every other value', function() {
-    eq(RA.isNotEmpty(0), true);
-    eq(RA.isNotEmpty(NaN), true);
-    eq(RA.isNotEmpty(['']), true);
+  it('should support placeholder to specify "gaps"', function() {
+    const isNotEmpty = RA.isNotEmpty(R.__);
+
+    assert.isTrue(isNotEmpty(-1));
   });
 });