Skip to content

Allow stopping iteration via pMap.stop - #36

Closed
papb wants to merge 9 commits into
sindresorhus:mainfrom
papb:early-stop
Closed

Allow stopping iteration via pMap.stop#36
papb wants to merge 9 commits into
sindresorhus:mainfrom
papb:early-stop

Conversation

@papb

@papb papb commented Jun 5, 2021

Copy link
Copy Markdown
Contributor

Closes #31

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

Comment thread index.js
Comment thread index.js Outdated
Comment thread index.d.ts

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

type MaybeWrappedInStop<NewElement> = NewElement | StopValueWrapper<NewElement>;

Copy link
Copy Markdown
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.

@papb papb Jun 7, 2021

Copy link
Copy Markdown
Contributor Author

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
Copy Markdown
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
Copy Markdown
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

@Richienb Richienb Jun 30, 2021

Copy link
Copy Markdown
Contributor

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.

Comment thread index.d.ts
@@ -1,3 +1,7 @@
declare const stop: unique symbol;

export type StopSymbol = typeof stop;

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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

Copy link
Copy Markdown

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?

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

@default false

Copy link
Copy Markdown
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?

@papb papb Jun 7, 2021

Copy link
Copy Markdown
Contributor Author

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment thread index.js Outdated
Comment thread index.d.ts
/**
Options to configure what `pMap` must do with any concurrent ongoing mappings at the moment `stop` is called.
*/
readonly ongoingMappings?: OngoingMappingsStopOptions;

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Owner

@Richienb Would you be able to help review? 🙏

Comment thread index.d.ts
mapper: Mapper<Element, NewElement>,
options?: Options
): Promise<NewElement[]>;
declare const pMap: {

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment thread readme.md Outdated

### 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
Copy Markdown
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
Copy Markdown
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?

Comment thread index.d.ts
/**
Options to configure what `pMap` must do with any concurrent ongoing mappings at the moment `stop` is called.
*/
readonly ongoingMappings?: OngoingMappingsStopOptions;

Copy link
Copy Markdown
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.

Comment thread readme.md Outdated
Comment thread index.d.ts
@@ -1,3 +1,7 @@
declare const stop: unique symbol;

export type StopSymbol = typeof stop;

Copy link
Copy Markdown
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

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

@default false

Copy link
Copy Markdown
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.

Comment thread index.d.ts
mapper: Mapper<Element, NewElement>,
options?: Options
): Promise<NewElement[]>;
declare const pMap: {

Copy link
Copy Markdown
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.

Comment thread index.d.ts

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

type MaybeWrappedInStop<NewElement> = NewElement | StopValueWrapper<NewElement>;

Copy link
Copy Markdown
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

Comment thread index.d.ts
/**
Options to configure what `pMap` must do with any concurrent ongoing mappings at the moment `stop` is called.
*/
readonly ongoingMappings?: OngoingMappingsStopOptions;

Copy link
Copy Markdown
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
Copy Markdown
Owner

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

@sindresorhus

Copy link
Copy Markdown
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