86
*/
87
export function cancelPreviousCalls<
89
>
TArgs extends unknown[],
90
>
TReturn,
91
>
>(
92
>
_proto: TObject,
93
>
methodName: string,
94
>
descriptor: TypedPropertyDescriptor<TWithOptionalCancellationToken<(...args: TArgs) => TReturn>>,
95
>
) {
96
>
const originalMethod = descriptor.value;
97
>
98
>
assertDefined(
99
>
originalMethod,
100
>
`Method '${methodName}' is not defined.`,
101
>
);
102
>
103
>
// we create the global map that contains `TObjectRecord` for each object instance that
104
>
// uses this decorator, which itself contains a `{method name} -> TMethodRecord` mapping
105
>
// for each decorated method on the object; the `TMethodRecord` record stores current
106
>
// `cancellationTokenSource`, token of which was passed to the previous call of the method
107
>
const objectRecords = new WeakMap<TObject, DisposableMap<string, CancellationTokenSource>>();
108
>
109
>
// decorate the original method with the following logic that upon a new invocation
110
>
// of the method cancels the cancellation token that was passed to a previous call
111
>
descriptor.value = function (
112
>
this: TObject,
113
>
...args: Parameters<typeof originalMethod>
114
>
): TReturn {
115
>
// get or create a record for the current object instance
116
>
// the creation is done once per each object instance
117
>
let record = objectRecords.get(this);
118
>
if (!record) {
119
>
record = new DisposableMap();
120
>
objectRecords.set(this, record);
121
>
122
>
this._register({
123
>
dispose: () => {
124
>
objectRecords.get(this)?.dispose();
125
>
objectRecords.delete(this);
126
>
},
127
>
});
128
>
}
129
>
130
>
// when the decorated method is called again and there is a cancellation token
131
>
// source exists from a previous call, cancel and dispose it, then remove it
132
>
record.get(methodName)?.dispose(true);
133
>
134
>
// now we need to provide a cancellation token to the original method
135
>
// as the last argument, there are two cases to consider:
136
>
// - (common case) the arguments list does not have a cancellation token
137
>
// as the last argument, - in this case we need to add a new one
138
>
// - (possible case) - the arguments list already has a cancellation token
139
>
// as the last argument, - in this case we need to reuse the token when
140
>
// we create ours, and replace the old token with the new one
141
>
// therefore,
142
>
143
>
// get the last argument of the arguments list and if it is present,
144
>
// reuse it as the token for the new cancellation token source
145
>
const lastArgument = (args.length > 0)
146
>
? args[args.length - 1]
147
: undefined;
149
? lastArgument
151
>
152
>
const cancellationSource = new CancellationTokenSource(token);
153
>
record.set(methodName, cancellationSource);
154
>
155
>
// then update or add cancellation token at the end of the arguments list
156
>
if (CancellationToken.isCancellationToken(lastArgument)) {
157
args[args.length - 1] = cancellationSource.token;
159
>
args.push(cancellationSource.token);
160
>
}
161
>
162
>
// finally invoke the original method passing original arguments and
163
>
// the new cancellation token at the end of the arguments list
164
>
return originalMethod.call(this, ...args);
165
>
};
166
>
167
>
return descriptor;
168
>
}