@angular-devkit/architect
Version:
Angular Build Facade
382 lines (381 loc) • 16.3 kB
JavaScript
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SimpleScheduler = exports.JobOutputSchemaValidationError = exports.JobInboundMessageSchemaValidationError = exports.JobArgumentSchemaValidationError = void 0;
const core_1 = require("@angular-devkit/core");
const rxjs_1 = require("rxjs");
const api_1 = require("./api");
const exception_1 = require("./exception");
class JobArgumentSchemaValidationError extends core_1.schema.SchemaValidationException {
constructor(errors) {
super(errors, 'Job Argument failed to validate. Errors: ');
}
}
exports.JobArgumentSchemaValidationError = JobArgumentSchemaValidationError;
class JobInboundMessageSchemaValidationError extends core_1.schema.SchemaValidationException {
constructor(errors) {
super(errors, 'Job Inbound Message failed to validate. Errors: ');
}
}
exports.JobInboundMessageSchemaValidationError = JobInboundMessageSchemaValidationError;
class JobOutputSchemaValidationError extends core_1.schema.SchemaValidationException {
constructor(errors) {
super(errors, 'Job Output failed to validate. Errors: ');
}
}
exports.JobOutputSchemaValidationError = JobOutputSchemaValidationError;
function _jobShare() {
// This is the same code as a `shareReplay()` operator, but uses a dumber Subject rather than a
// ReplaySubject.
return (source) => {
let refCount = 0;
let subject;
let hasError = false;
let isComplete = false;
let subscription;
return new rxjs_1.Observable((subscriber) => {
let innerSub;
refCount++;
if (!subject) {
subject = new rxjs_1.Subject();
innerSub = subject.subscribe(subscriber);
subscription = source.subscribe({
next(value) {
subject.next(value);
},
error(err) {
hasError = true;
subject.error(err);
},
complete() {
isComplete = true;
subject.complete();
},
});
}
else {
innerSub = subject.subscribe(subscriber);
}
return () => {
refCount--;
innerSub.unsubscribe();
if (subscription && refCount === 0 && (isComplete || hasError)) {
subscription.unsubscribe();
}
};
});
};
}
/**
* Simple scheduler. Should be the base of all registries and schedulers.
*/
class SimpleScheduler {
_jobRegistry;
_schemaRegistry;
_internalJobDescriptionMap = new Map();
_queue = [];
_pauseCounter = 0;
constructor(_jobRegistry, _schemaRegistry = new core_1.schema.CoreSchemaRegistry()) {
this._jobRegistry = _jobRegistry;
this._schemaRegistry = _schemaRegistry;
}
_getInternalDescription(name) {
const maybeHandler = this._internalJobDescriptionMap.get(name);
if (maybeHandler !== undefined) {
return (0, rxjs_1.of)(maybeHandler);
}
const handler = this._jobRegistry.get(name);
return handler.pipe((0, rxjs_1.switchMap)((handler) => {
if (handler === null) {
return (0, rxjs_1.of)(null);
}
const description = {
// Make a copy of it to be sure it's proper JSON.
...JSON.parse(JSON.stringify(handler.jobDescription)),
name: handler.jobDescription.name || name,
argument: handler.jobDescription.argument || true,
input: handler.jobDescription.input || true,
output: handler.jobDescription.output || true,
channels: handler.jobDescription.channels || {},
};
const handlerWithExtra = Object.assign(handler.bind(undefined), {
jobDescription: description,
argumentV: this._schemaRegistry.compile(description.argument),
inputV: this._schemaRegistry.compile(description.input),
outputV: this._schemaRegistry.compile(description.output),
});
this._internalJobDescriptionMap.set(name, handlerWithExtra);
return (0, rxjs_1.of)(handlerWithExtra);
}));
}
/**
* Get a job description for a named job.
*
* @param name The name of the job.
* @returns A description, or null if the job is not registered.
*/
getDescription(name) {
return (0, rxjs_1.concat)(this._getInternalDescription(name).pipe((0, rxjs_1.map)((x) => x && x.jobDescription)), (0, rxjs_1.of)(null)).pipe((0, rxjs_1.first)());
}
/**
* Returns true if the job name has been registered.
* @param name The name of the job.
* @returns True if the job exists, false otherwise.
*/
has(name) {
return this.getDescription(name).pipe((0, rxjs_1.map)((x) => x !== null));
}
/**
* Pause the scheduler, temporary queueing _new_ jobs. Returns a resume function that should be
* used to resume execution. If multiple `pause()` were called, all their resume functions must
* be called before the Scheduler actually starts new jobs. Additional calls to the same resume
* function will have no effect.
*
* Jobs already running are NOT paused. This is pausing the scheduler only.
*/
pause() {
let called = false;
this._pauseCounter++;
return () => {
if (!called) {
called = true;
if (--this._pauseCounter == 0) {
// Resume the queue.
const q = this._queue;
this._queue = [];
q.forEach((fn) => fn());
}
}
};
}
/**
* Schedule a job to be run, using its name.
* @param name The name of job to be run.
* @param argument The argument to send to the job when starting it.
* @param options Scheduling options.
* @returns The Job being run.
*/
schedule(name, argument, options) {
if (this._pauseCounter > 0) {
const waitable = new rxjs_1.Subject();
this._queue.push(() => waitable.complete());
return this._scheduleJob(name, argument, options || {}, waitable);
}
return this._scheduleJob(name, argument, options || {}, rxjs_1.EMPTY);
}
/**
* Filter messages.
* @private
*/
_filterJobOutboundMessages(message, state) {
switch (message.kind) {
case api_1.JobOutboundMessageKind.OnReady:
return state == api_1.JobState.Queued;
case api_1.JobOutboundMessageKind.Start:
return state == api_1.JobState.Ready;
case api_1.JobOutboundMessageKind.End:
return state == api_1.JobState.Started || state == api_1.JobState.Ready;
}
return true;
}
/**
* Return a new state. This is just to simplify the reading of the _createJob method.
* @private
*/
_updateState(message, state) {
switch (message.kind) {
case api_1.JobOutboundMessageKind.OnReady:
return api_1.JobState.Ready;
case api_1.JobOutboundMessageKind.Start:
return api_1.JobState.Started;
case api_1.JobOutboundMessageKind.End:
return api_1.JobState.Ended;
}
return state;
}
/**
* Create the job.
* @private
*/
// eslint-disable-next-line max-lines-per-function
_createJob(name, argument, handler, inboundBus, outboundBus) {
const schemaRegistry = this._schemaRegistry;
const channelsSubject = new Map();
const channels = new Map();
let state = api_1.JobState.Queued;
let pingId = 0;
// Create the input channel by having a filter.
const input = new rxjs_1.Subject();
input
.pipe((0, rxjs_1.concatMap)((message) => handler.pipe((0, rxjs_1.switchMap)(async (handler) => {
if (handler === null) {
throw new exception_1.JobDoesNotExistException(name);
}
const validator = await handler.inputV;
return validator(message);
}))), (0, rxjs_1.filter)((result) => result.success), (0, rxjs_1.map)((result) => result.data))
.subscribe((value) => inboundBus.next({ kind: api_1.JobInboundMessageKind.Input, value }));
outboundBus = (0, rxjs_1.concat)(outboundBus,
// Add an End message at completion. This will be filtered out if the job actually send an
// End.
handler.pipe((0, rxjs_1.switchMap)((handler) => {
if (handler) {
return (0, rxjs_1.of)({
kind: api_1.JobOutboundMessageKind.End,
description: handler.jobDescription,
});
}
else {
return rxjs_1.EMPTY;
}
}))).pipe((0, rxjs_1.filter)((message) => this._filterJobOutboundMessages(message, state)),
// Update internal logic and Job<> members.
(0, rxjs_1.tap)((message) => {
// Update the state.
state = this._updateState(message, state);
switch (message.kind) {
case api_1.JobOutboundMessageKind.ChannelCreate: {
const maybeSubject = channelsSubject.get(message.name);
// If it doesn't exist or it's closed on the other end.
if (!maybeSubject) {
const s = new rxjs_1.Subject();
channelsSubject.set(message.name, s);
channels.set(message.name, s.asObservable());
}
break;
}
case api_1.JobOutboundMessageKind.ChannelMessage: {
const maybeSubject = channelsSubject.get(message.name);
if (maybeSubject) {
maybeSubject.next(message.message);
}
break;
}
case api_1.JobOutboundMessageKind.ChannelComplete: {
const maybeSubject = channelsSubject.get(message.name);
if (maybeSubject) {
maybeSubject.complete();
channelsSubject.delete(message.name);
}
break;
}
case api_1.JobOutboundMessageKind.ChannelError: {
const maybeSubject = channelsSubject.get(message.name);
if (maybeSubject) {
maybeSubject.error(message.error);
channelsSubject.delete(message.name);
}
break;
}
}
}, () => {
state = api_1.JobState.Errored;
}),
// Do output validation (might include default values so this might have side
// effects). We keep all messages in order.
(0, rxjs_1.concatMap)((message) => {
if (message.kind !== api_1.JobOutboundMessageKind.Output) {
return (0, rxjs_1.of)(message);
}
return handler.pipe((0, rxjs_1.switchMap)(async (handler) => {
if (handler === null) {
throw new exception_1.JobDoesNotExistException(name);
}
const validate = await handler.outputV;
const output = await validate(message.value);
if (!output.success) {
throw new JobOutputSchemaValidationError(output.errors);
}
return {
...message,
output: output.data,
};
}));
}), _jobShare());
const output = outboundBus.pipe((0, rxjs_1.filter)((x) => x.kind == api_1.JobOutboundMessageKind.Output), (0, rxjs_1.map)((x) => x.value), (0, rxjs_1.shareReplay)(1));
// Return the Job.
return {
get state() {
return state;
},
argument,
description: handler.pipe((0, rxjs_1.switchMap)((handler) => {
if (handler === null) {
throw new exception_1.JobDoesNotExistException(name);
}
else {
return (0, rxjs_1.of)(handler.jobDescription);
}
})),
output,
getChannel(name, schema = true) {
let maybeObservable = channels.get(name);
if (!maybeObservable) {
const s = new rxjs_1.Subject();
channelsSubject.set(name, s);
channels.set(name, s.asObservable());
maybeObservable = s.asObservable();
}
return maybeObservable.pipe(
// Keep the order of messages.
(0, rxjs_1.concatMap)((message) => {
return (0, rxjs_1.from)(schemaRegistry.compile(schema)).pipe((0, rxjs_1.switchMap)((validate) => validate(message)), (0, rxjs_1.filter)((x) => x.success), (0, rxjs_1.map)((x) => x.data));
}));
},
ping() {
const id = pingId++;
inboundBus.next({ kind: api_1.JobInboundMessageKind.Ping, id });
return outboundBus.pipe((0, rxjs_1.filter)((x) => x.kind === api_1.JobOutboundMessageKind.Pong && x.id == id), (0, rxjs_1.first)(), (0, rxjs_1.ignoreElements)());
},
stop() {
inboundBus.next({ kind: api_1.JobInboundMessageKind.Stop });
},
input,
inboundBus,
outboundBus,
};
}
_scheduleJob(name, argument, options, waitable) {
// Get handler first, since this can error out if there's no handler for the job name.
const handler = this._getInternalDescription(name);
const optionsDeps = (options && options.dependencies) || [];
const dependencies = Array.isArray(optionsDeps) ? optionsDeps : [optionsDeps];
const inboundBus = new rxjs_1.Subject();
const outboundBus = (0, rxjs_1.concat)(
// Wait for dependencies, make sure to not report messages from dependencies. Subscribe to
// all dependencies at the same time so they run concurrently.
(0, rxjs_1.merge)(...dependencies.map((x) => x.outboundBus)).pipe((0, rxjs_1.ignoreElements)()),
// Wait for pause() to clear (if necessary).
waitable, (0, rxjs_1.from)(handler).pipe((0, rxjs_1.switchMap)((handler) => new rxjs_1.Observable((subscriber) => {
if (!handler) {
throw new exception_1.JobDoesNotExistException(name);
}
// Validate the argument.
return (0, rxjs_1.from)(handler.argumentV)
.pipe((0, rxjs_1.switchMap)((validate) => validate(argument)), (0, rxjs_1.switchMap)((output) => {
if (!output.success) {
throw new JobArgumentSchemaValidationError(output.errors);
}
const argument = output.data;
const description = handler.jobDescription;
subscriber.next({ kind: api_1.JobOutboundMessageKind.OnReady, description });
const context = {
description,
dependencies: [...dependencies],
inboundBus: inboundBus.asObservable(),
scheduler: this,
};
return handler(argument, context);
}))
.subscribe(subscriber);
}))));
return this._createJob(name, argument, handler, inboundBus, outboundBus);
}
}
exports.SimpleScheduler = SimpleScheduler;
;