chrome-devtools-frontend
Version:
Chrome DevTools UI
126 lines (112 loc) • 4.02 kB
text/typescript
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import {AsyncDirective} from '../async-directive.js';
import {
clearPart,
insertPart,
setChildPartValue,
} from '../directive-helpers.js';
import {
directive,
DirectiveParameters,
PartInfo,
PartType,
} from '../directive.js';
import {ChildPart, noChange} from '../lit-html.js';
type Mapper<T> = (v: T, index?: number) => unknown;
class AsyncAppendDirective extends AsyncDirective {
private _value?: AsyncIterable<unknown>;
private _reconnectResolver?: () => void;
private _reconnectPromise?: Promise<void>;
constructor(partInfo: PartInfo) {
super(partInfo);
if (partInfo.type !== PartType.CHILD) {
throw new Error('asyncAppend can only be used in child expressions');
}
}
// @ts-expect-error value not used, but we want a nice parameter for docs
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render<T>(value: AsyncIterable<T>, _mapper?: Mapper<T>) {
return noChange;
}
update(part: ChildPart, [value, mapper]: DirectiveParameters<this>) {
// If we've already set up this particular iterable, we don't need
// to do anything.
if (value === this._value) {
return;
}
this._value = value;
this.__iterate(part, mapper);
return noChange;
}
// Separate function to avoid an iffe in update; update can't be async
// because its return value must be `noChange`
private async __iterate(part: ChildPart, mapper?: Mapper<unknown>) {
let i = 0;
const {_value: value} = this;
for await (let v of value!) {
// Check to make sure that value is the still the current value of
// the part, and if not bail because a new value owns this part
if (this._value !== value) {
break;
}
// If we were disconnected, pause until reconnected
if (this._reconnectPromise) {
await this._reconnectPromise;
}
// When we get the first value, clear the part. This lets the
// previous value display until we can replace it.
if (i === 0) {
clearPart(part);
}
// As a convenience, because functional-programming-style
// transforms of iterables and async iterables requires a library,
// we accept a mapper function. This is especially convenient for
// rendering a template for each item.
if (mapper !== undefined) {
v = mapper(v, i);
}
const newPart = insertPart(part);
setChildPartValue(newPart, v);
i++;
}
}
disconnected() {
// Pause iteration while disconnected
this._reconnectPromise = new Promise(
(resolve) => (this._reconnectResolver = resolve)
);
}
reconnected() {
// Resume iteration when reconnected
this._reconnectPromise = undefined;
this._reconnectResolver!();
}
}
/**
* A directive that renders the items of an async iterable[1], appending new
* values after previous values, similar to the built-in support for iterables.
* This directive is usable only in child expressions.
*
* Async iterables are objects with a [Symbol.asyncIterator] method, which
* returns an iterator who's `next()` method returns a Promise. When a new
* value is available, the Promise resolves and the value is appended to the
* Part controlled by the directive. If another value other than this
* directive has been set on the Part, the iterable will no longer be listened
* to and new values won't be written to the Part.
*
* [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of
*
* @param value An async iterable
* @param mapper An optional function that maps from (value, index) to another
* value. Useful for generating templates for each item in the iterable.
*/
export const asyncAppend = directive(AsyncAppendDirective);
/**
* The type of the class that powers this directive. Necessary for naming the
* directive's return type.
*/
export type {AsyncAppendDirective};