Iterables: add and use filter

This commit is contained in:
Evan Hahn 2021-05-18 13:07:00 -05:00 committed by Scott Nonnenberg
parent 2abc331058
commit 392822372b
3 changed files with 142 additions and 42 deletions

View file

@ -50,6 +50,7 @@ import { handleMessageSend } from '../util/handleMessageSend';
import { getConversationMembers } from '../util/getConversationMembers'; import { getConversationMembers } from '../util/getConversationMembers';
import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor'; import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { filter, map, take } from '../util/iterables';
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -2855,53 +2856,54 @@ export class ConversationModel extends window.Backbone
sticker: WhatIsThis sticker: WhatIsThis
): Promise<WhatIsThis> { ): Promise<WhatIsThis> {
if (attachments && attachments.length) { if (attachments && attachments.length) {
return Promise.all( const validAttachments = filter(
attachments attachments,
.filter( attachment => attachment && !attachment.pending && !attachment.error
attachment => attachment && !attachment.pending && !attachment.error );
) const attachmentsToUse = take(validAttachments, 1);
.slice(0, 1)
.map(async attachment => {
const { fileName, thumbnail, contentType } = attachment;
return { return Promise.all(
contentType, map(attachmentsToUse, async attachment => {
// Our protos library complains about this field being undefined, so we const { fileName, thumbnail, contentType } = attachment;
// force it to null
fileName: fileName || null, return {
thumbnail: thumbnail contentType,
? { // Our protos library complains about this field being undefined, so we force
...(await loadAttachmentData(thumbnail)), // it to null
objectUrl: getAbsoluteAttachmentPath(thumbnail.path), fileName: fileName || null,
} thumbnail: thumbnail
: null, ? {
}; ...(await loadAttachmentData(thumbnail)),
}) objectUrl: getAbsoluteAttachmentPath(thumbnail.path),
}
: null,
};
})
); );
} }
if (preview && preview.length) { if (preview && preview.length) {
return Promise.all( const validPreviews = filter(preview, item => item && item.image);
preview const previewsToUse = take(validPreviews, 1);
.filter(item => item && item.image)
.slice(0, 1)
.map(async attachment => {
const { image } = attachment;
const { contentType } = image;
return { return Promise.all(
contentType, map(previewsToUse, async attachment => {
// Our protos library complains about this field being undefined, so we const { image } = attachment;
// force it to null const { contentType } = image;
fileName: null,
thumbnail: image return {
? { contentType,
...(await loadAttachmentData(image)), // Our protos library complains about this field being undefined, so we
objectUrl: getAbsoluteAttachmentPath(image.path), // force it to null
} fileName: null,
: null, thumbnail: image
}; ? {
}) ...(await loadAttachmentData(image)),
objectUrl: getAbsoluteAttachmentPath(image.path),
}
: null,
};
})
); );
} }

View file

@ -4,7 +4,7 @@
import { assert } from 'chai'; import { assert } from 'chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { isIterable, size, map, take } from '../../util/iterables'; import { isIterable, size, filter, map, take } from '../../util/iterables';
describe('iterable utilities', () => { describe('iterable utilities', () => {
describe('isIterable', () => { describe('isIterable', () => {
@ -82,6 +82,61 @@ describe('iterable utilities', () => {
}); });
}); });
describe('filter', () => {
it('returns an empty iterable when passed an empty iterable', () => {
const fn = sinon.fake();
assert.deepEqual([...filter([], fn)], []);
assert.deepEqual([...filter(new Set(), fn)], []);
assert.deepEqual([...filter(new Map(), fn)], []);
sinon.assert.notCalled(fn);
});
it('returns a new iterator with some values removed', () => {
const isOdd = sinon.fake((n: number) => Boolean(n % 2));
const result = filter([1, 2, 3, 4], isOdd);
sinon.assert.notCalled(isOdd);
assert.deepEqual([...result], [1, 3]);
assert.notInstanceOf(result, Array);
sinon.assert.callCount(isOdd, 4);
});
it('can filter an infinite iterable', () => {
const everyNumber = {
*[Symbol.iterator]() {
for (let i = 0; true; i += 1) {
yield i;
}
},
};
const isOdd = (n: number) => Boolean(n % 2);
const result = filter(everyNumber, isOdd);
const iterator = result[Symbol.iterator]();
assert.deepEqual(iterator.next(), { value: 1, done: false });
assert.deepEqual(iterator.next(), { value: 3, done: false });
assert.deepEqual(iterator.next(), { value: 5, done: false });
assert.deepEqual(iterator.next(), { value: 7, done: false });
});
it('respects TypeScript type assertion signatures', () => {
// This tests TypeScript, not the actual runtime behavior.
function isString(value: unknown): value is string {
return typeof value === 'string';
}
const input: Array<unknown> = [1, 'two', 3, 'four'];
const result: Iterable<string> = filter(input, isString);
assert.deepEqual([...result], ['two', 'four']);
});
});
describe('map', () => { describe('map', () => {
it('returns an empty iterable when passed an empty iterable', () => { it('returns an empty iterable when passed an empty iterable', () => {
const fn = sinon.fake(); const fn = sinon.fake();

View file

@ -28,6 +28,49 @@ export function size(iterable: Iterable<unknown>): number {
return result; return result;
} }
export function filter<T, S extends T>(
iterable: Iterable<T>,
predicate: (value: T) => value is S
): Iterable<S>;
export function filter<T>(
iterable: Iterable<T>,
predicate: (value: T) => unknown
): Iterable<T>;
export function filter<T>(
iterable: Iterable<T>,
predicate: (value: T) => unknown
): Iterable<T> {
return new FilterIterable(iterable, predicate);
}
class FilterIterable<T> implements Iterable<T> {
constructor(
private readonly iterable: Iterable<T>,
private readonly predicate: (value: T) => unknown
) {}
[Symbol.iterator](): Iterator<T> {
return new FilterIterator(this.iterable[Symbol.iterator](), this.predicate);
}
}
class FilterIterator<T> implements Iterator<T> {
constructor(
private readonly iterator: Iterator<T>,
private readonly predicate: (value: T) => unknown
) {}
next(): IteratorResult<T> {
// eslint-disable-next-line no-constant-condition
while (true) {
const nextIteration = this.iterator.next();
if (nextIteration.done || this.predicate(nextIteration.value)) {
return nextIteration;
}
}
}
}
export function map<T, ResultT>( export function map<T, ResultT>(
iterable: Iterable<T>, iterable: Iterable<T>,
fn: (value: T) => ResultT fn: (value: T) => ResultT