Skip to content

Commit 3ac1cc3

Browse files
hanslvikerman
authored andcommitted
feat(@angular-devkit/architect): add scheduleTargetAndForget function
It came up with Nrwl that this is a common pattern; someone wants to schedule a target but does not want to manage the run himself. This function cancels the run when the Observable is unsubscribed from (which is not the case for a traditional run). Because stop logic can be asynchronous, we need to add a teardown logic handler to the context, which turns out to be useful for other cases as well.
1 parent 6a0b3fe commit 3ac1cc3

File tree

2 files changed

+57
-3
lines changed

2 files changed

+57
-3
lines changed

packages/angular_devkit/architect/src/api.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import { experimental, json, logging } from '@angular-devkit/core';
9-
import { Observable } from 'rxjs';
9+
import { Observable, from } from 'rxjs';
10+
import { switchMap } from 'rxjs/operators';
1011
import { Schema as RealBuilderInput, Target as RealTarget } from './input-schema';
1112
import { Schema as RealBuilderOutput } from './output-schema';
1213
import { Schema as RealBuilderProgress, State as BuilderProgressState } from './progress-schema';
@@ -211,6 +212,11 @@ export interface BuilderContext {
211212
* @param status Update the status string. If omitted the status string is not modified.
212213
*/
213214
reportProgress(current: number, total?: number, status?: string): void;
215+
216+
/**
217+
* Add teardown logic to this Context, so that when it's being stopped it will execute teardown.
218+
*/
219+
addTeardown(teardown: () => (Promise<void> | void)): void;
214220
}
215221

216222

@@ -268,3 +274,41 @@ export function targetFromTargetString(str: string): Target {
268274
...(tuple[2] !== undefined) && { configuration: tuple[2] },
269275
};
270276
}
277+
278+
/**
279+
* Schedule a target, and forget about its run. This will return an observable of outputs, that
280+
* as a a teardown will stop the target from running. This means that the Run object this returns
281+
* should not be shared.
282+
*
283+
* The reason this is not part of the Context interface is to keep the Context as normal form as
284+
* possible. This is really an utility that people would implement in their project.
285+
*
286+
* @param context The context of your current execution.
287+
* @param target The target to schedule.
288+
* @param overrides Overrides that are used in the target.
289+
* @param scheduleOptions Additional scheduling options.
290+
*/
291+
export function scheduleTargetAndForget(
292+
context: BuilderContext,
293+
target: Target,
294+
overrides?: json.JsonObject,
295+
scheduleOptions?: ScheduleOptions,
296+
): Observable<BuilderOutput> {
297+
let resolve: (() => void) | null = null;
298+
const promise = new Promise<void>(r => resolve = r);
299+
context.addTeardown(() => promise);
300+
301+
return from(context.scheduleTarget(target, overrides, scheduleOptions)).pipe(
302+
switchMap(run => new Observable<BuilderOutput>(observer => {
303+
const subscription = run.output.subscribe(observer);
304+
305+
return () => {
306+
subscription.unsubscribe();
307+
// We can properly ignore the floating promise as it's a "reverse" promise; the teardown
308+
// is waiting for the resolve.
309+
// tslint:disable-next-line:no-floating-promises
310+
run.stop().then(resolve);
311+
};
312+
})),
313+
);
314+
}

packages/angular_devkit/architect/src/create-builder.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export function createBuilder<
3737
const progressChannel = context.createChannel('progress');
3838
const logChannel = context.createChannel('log');
3939
let currentState: BuilderProgressState = BuilderProgressState.Stopped;
40+
const teardownLogics: Array<() => (PromiseLike<void> | void)> = [];
41+
let tearingDown = false;
4042
let current = 0;
4143
let status = '';
4244
let total = 1;
@@ -72,10 +74,15 @@ export function createBuilder<
7274
i => {
7375
switch (i.kind) {
7476
case experimental.jobs.JobInboundMessageKind.Stop:
75-
observer.complete();
77+
// Run teardown logic then complete.
78+
tearingDown = true;
79+
Promise.all(teardownLogics.map(fn => fn() || Promise.resolve()))
80+
.then(() => observer.complete(), err => observer.error(err));
7681
break;
7782
case experimental.jobs.JobInboundMessageKind.Input:
78-
onInput(i.value);
83+
if (!tearingDown) {
84+
onInput(i.value);
85+
}
7986
break;
8087
}
8188
},
@@ -159,6 +166,9 @@ export function createBuilder<
159166
progress({ state: currentState, current, total, status }, context);
160167
}
161168
},
169+
addTeardown(teardown: () => (Promise<void> | void)): void {
170+
teardownLogics.push(teardown);
171+
},
162172
};
163173

164174
context.reportRunning();

0 commit comments

Comments
 (0)