/
async-replace.ts
116 lines (105 loc) · 4.02 KB
/
async-replace.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import {ChildPart, noChange} from '../lit-html.js';
import {
AsyncDirective,
directive,
DirectiveParameters,
} from '../async-directive.js';
import {Pauser, PseudoWeakRef, forAwaitOf} from './private-async-helpers.js';
type Mapper<T> = (v: T, index?: number) => unknown;
export class AsyncReplaceDirective extends AsyncDirective {
private __value?: AsyncIterable<unknown>;
private __weakThis = new PseudoWeakRef(this);
private __pauser = new Pauser();
// @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;
}
override update(
_part: ChildPart,
[value, mapper]: DirectiveParameters<this>
) {
// If our initial render occurs while disconnected, ensure that the pauser
// and weakThis are in the disconnected state
if (!this.isConnected) {
this.disconnected();
}
// If we've already set up this particular iterable, we don't need
// to do anything.
if (value === this.__value) {
return;
}
this.__value = value;
let i = 0;
const {__weakThis: weakThis, __pauser: pauser} = this;
// Note, the callback avoids closing over `this` so that the directive
// can be gc'ed before the promise resolves; instead `this` is retrieved
// from `weakThis`, which can break the hard reference in the closure when
// the directive disconnects
forAwaitOf(value, async (v: unknown) => {
// The while loop here handles the case that the connection state
// thrashes, causing the pauser to resume and then get re-paused
while (pauser.get()) {
await pauser.get();
}
// If the callback gets here and there is no `this`, it means that the
// directive has been disconnected and garbage collected and we don't
// need to do anything else
const _this = weakThis.deref();
if (_this !== undefined) {
// 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) {
return false;
}
// 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);
}
_this.commitValue(v, i);
i++;
}
return true;
});
return noChange;
}
// Override point for AsyncAppend to append rather than replace
protected commitValue(value: unknown, _index: number) {
this.setValue(value);
}
override disconnected() {
this.__weakThis.disconnect();
this.__pauser.pause();
}
override reconnected() {
this.__weakThis.reconnect(this);
this.__pauser.resume();
}
}
/**
* A directive that renders the items of an async iterable[1], replacing
* previous values with new values, so that only one value is ever rendered
* at a time. This directive may be used in any expression type.
*
* 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 rendered 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 asyncReplace = directive(AsyncReplaceDirective);