Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow stopping iteration via pMap.stop #36

Closed
wants to merge 9 commits into from

Conversation

papb
Copy link
Contributor

@papb papb commented Jun 5, 2021

Closes #31

Note: this PR also currently includes a bug fix for an edge case, see below. edit: no longer true, moved to #40

index.js Outdated Show resolved Hide resolved
index.js Outdated Show resolved Hide resolved

export type StopValueWrapper<NewElement> = NewElement extends any ? BaseStopValueWrapper<NewElement> : never;

type MaybeWrappedInStop<NewElement> = NewElement | StopValueWrapper<NewElement>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these 3 types? Not immediately clear why this complication is needed. We don't need to expose anything about stop to the user in the return value. For the user, it can just be an opaque object.

Copy link
Contributor Author

@papb papb Jun 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these 3 types? Not immediately clear why this complication is needed.

To be honest that was the simplest way I could make it work. All attempts to simplify this further caused a test in test-d.ts to fail. In particular, I observed that TypeScript does not realize automatically that { x: A } | { x: B } is the same as { x: A | B }, so I had to write this extends any ? hack.

For the user, it can just be an opaque object.

Nice idea. Do you have any favorite way of declaring an opaque object in TypeScript? Recenly I've seen Record<never, never>, does this sound good?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to expose anything about stop to the user in the return value. For the user, it can just be an opaque object.

Actually, on second thought, I think we have to, because as soon as the user uses return pMap.stop() inside mapper, TypeScript will infer the return type of mapper differently, and we need TS to infer it to something that we can unwrap later in order to provide a correct return type for the pMap call itself. This was quite complicated to achieve to be honest. I don't see how it would be possible if we simplify the result of .stop to a constant type such as Record<never, never>.

Let's merge it this way, currently, and you can open an issue to improve typings? Then if someone finds a way to do it without breaking the current TS tests I added, it can be done later. What do you think?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just do what was done here: c9c0882 (Exclude the symbol from the return type)

// @Richienb

Copy link
Contributor

@Richienb Richienb Jun 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a stop value is recieved, it could set a variable which causes future resolutions of other items to avoid adding new values or creating new calls. Then, the array can be processed and resolved.

@@ -1,3 +1,7 @@
declare const stop: unique symbol;

export type StopSymbol = typeof stop;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see StopSymbol used anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on your idea above of not exposing the existence of { [stop] } in the returned object, perhaps this is indeed not needed. The reason I first added this was to have a way to expose it to the user, since for example in find-up it is also exposed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I undo this, then?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I undo this, then?

Yes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about passing options param for exposing stop in the returned object?

index.d.ts Outdated Show resolved Hide resolved
/**
Whether or not to remove all holes from the result array (caused by pending mappings).

@default false
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on making this true by default?

Copy link
Contributor Author

@papb papb Jun 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine by me. I don't have any strong opinion. Personally, I will probably set this option explicitly whenever I use .stop, regardless of what is the default. Do you want me to change it to true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Richienb do you have any opinion on what would be the best default value here?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets change it to true then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets change it to true then.

Agree, to match its behaviour with pMapSkip which could have left holes but didn't.

index.js Outdated Show resolved Hide resolved
/**
Options to configure what `pMap` must do with any concurrent ongoing mappings at the moment `stop` is called.
*/
readonly ongoingMappings?: OngoingMappingsStopOptions;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there's any way we could flatten the options object:

  • value
  • collpasePending
  • fillPending

Needs better names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to group them. In the future, if support for canceling promises and AbortController is added, they might probably need options that could also be added to this group.

Do you also want better names for the way I've currently done it? Or you said "better names" only for your suggestion itself?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like this:

  • value Value to resolve with
  • defaultValue Value to set when the promise hasn't been run
    What if the consumer wants it to be an empty value? Maybe this could be in the form of passing a pMap.stop.empty symbol
  • stopRunning Whether currently running promises should be allowed to finish
  • removeIncomplete Whether empty items in the resulting array where there should have been values if all the promises had run should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think the non-flattened way is better, because value is about the current iteration, while everything else is about configuring what happens to the rest of the mapping itself. Maybe instead of ongoingMappings, one of:

  • rest
  • restConfig
  • restOptions
  • whatToDoWithOngoingMappings
  • mappingsStillRunning
  • inProgressBehavior
  • inProgressConfig

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the consumer wants it to be an empty value? Maybe this could be in the form of passing a pMap.stop.empty symbol

I think this particular behavior could be left for another PR. I've never seen good uses for holes in arrays, they're an obscure feature of the language, if I'm not mistaken TypeScript for example pretends they don't exist. Sindre also said it here. Although if you found a good use case for them, I'd be interested in knowing more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, we can stick with the nested option. I'm not really sold on most of these names though. The problem is that it applies to any mappings, not just running ones, but also pending ones. For example, if the concurrency option is used, not all remaining mappings are executing when it's stopped. I think the only one that is usable is rest, so I guess we can go with that.

@sindresorhus
Copy link
Owner

@Richienb Would you be able to help review? 🙏

mapper: Mapper<Element, NewElement>,
options?: Options
): Promise<NewElement[]>;
declare const pMap: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of defining a constant do this:

export function stop(...);

export default function pMap(...);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, I tried to do it this way on 941ed8b but TypeScript tests failed, so I reverted it via 31be4be, what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests failed because the way the function is imported changed:

- import pMap from 'p-map';
- console.log(pMap, pMap.stop);
+ import pMap, {stop} from 'p-map';
+ console.log(pMap, stop);

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per c9c0882, the symbol should be a named export called pMapStop.

readme.md Outdated
@@ -72,6 +99,44 @@ Default: `true`

When set to `false`, instead of stopping when a promise rejects, it will wait for all the promises to settle and then reject with an [aggregated error](https://github.com/sindresorhus/aggregate-error) containing all the errors from the rejected promises.

### pMap.stop(options?)

Creates a special object that indicates to `pMap` that iteration must stop immediately. This object should just be returned from within the mapper (and not used directly for anything).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Creates a special object that indicates to `pMap` that iteration must stop immediately. This object should just be returned from within the mapper (and not used directly for anything).
Returns a value that can be returned from the `mapper` function provided to `pMap` to immediately stop iteration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I further reworded it taking inspiration in your text: be46075 - what do you think?

/**
Options to configure what `pMap` must do with any concurrent ongoing mappings at the moment `stop` is called.
*/
readonly ongoingMappings?: OngoingMappingsStopOptions;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like this:

  • value Value to resolve with
  • defaultValue Value to set when the promise hasn't been run
    What if the consumer wants it to be an empty value? Maybe this could be in the form of passing a pMap.stop.empty symbol
  • stopRunning Whether currently running promises should be allowed to finish
  • removeIncomplete Whether empty items in the resulting array where there should have been values if all the promises had run should be removed.

readme.md Outdated Show resolved Hide resolved
@@ -1,3 +1,7 @@
declare const stop: unique symbol;

export type StopSymbol = typeof stop;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I undo this, then?

Yes

/**
Whether or not to remove all holes from the result array (caused by pending mappings).

@default false
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets change it to true then.

mapper: Mapper<Element, NewElement>,
options?: Options
): Promise<NewElement[]>;
declare const pMap: {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per c9c0882, the symbol should be a named export called pMapStop.


export type StopValueWrapper<NewElement> = NewElement extends any ? BaseStopValueWrapper<NewElement> : never;

type MaybeWrappedInStop<NewElement> = NewElement | StopValueWrapper<NewElement>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just do what was done here: c9c0882 (Exclude the symbol from the return type)

// @Richienb

/**
Options to configure what `pMap` must do with any concurrent ongoing mappings at the moment `stop` is called.
*/
readonly ongoingMappings?: OngoingMappingsStopOptions;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, we can stick with the nested option. I'm not really sold on most of these names though. The problem is that it applies to any mappings, not just running ones, but also pending ones. For example, if the concurrency option is used, not all remaining mappings are executing when it's stopped. I think the only one that is usable is rest, so I guess we can go with that.

@sindresorhus
Copy link
Owner

@papb Friendly bump in case you forgot about this PR. Feel free to ignore if you're just busy ;)

@sindresorhus
Copy link
Owner

Closing to let someone else be able to work on this as this PR is not moving forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow breaking from iterator
4 participants