@microsoft/windows-admin-center-sdk
Version:
Microsoft - Windows Admin Center Shell
261 lines (259 loc) • 10.9 kB
JavaScript
import { from, of, throwError } from 'rxjs';
import { catchError, filter, map, mergeMap, take } from 'rxjs/operators';
import { RpcForwardAutoClient } from './forward/rpc-forward-auto-client';
import { RpcForwardDownKey } from './forward/rpc-forward-model';
import { RpcForwardUpSubjectServer } from './forward/rpc-forward-up-subject-server';
/**
* RPC forwarder class.
* @dynamic
*/
export class RpcForwarder {
static rpcForwardUpSubjectServer;
static ready;
static forwardMap = new Map();
static waitForReady(rpc) {
// if we are ready to forward, just return an observable.
if (rpc.stateActive && RpcForwarder.ready) {
return of(null);
}
// if we are already waiting for the rpc, then return that observable
if (RpcForwarder.ready) {
return RpcForwarder.ready;
}
// if we are not ready to forward, then set up forwarding based on our window type and current rpc state
if (MsftSme.isShell()) {
// If we are shell then setup the the rpc subject server
RpcForwarder.rpcForwardUpSubjectServer = new RpcForwardUpSubjectServer(rpc);
RpcForwarder.rpcForwardUpSubjectServer.subject.subscribe(data => {
RpcForwarder.onForwardReceived(data.data).then(data.deferred.resolve, data.deferred.reject);
});
}
else {
if (!rpc.stateActive) {
// return the first rpc state change that sets the state to active.
// then register our rpc command before returning
RpcForwarder.ready = rpc.stateChanged
.pipe(filter(active => active), take(1), map(() => rpc.register(RpcForwardDownKey.command, RpcForwarder.onForwardReceived)));
return RpcForwarder.ready;
}
else {
// If we are not shell, then setup rpc registration for forwarded messages
rpc.register(RpcForwardDownKey.command, RpcForwarder.onForwardReceived);
}
}
RpcForwarder.ready = of(null);
return RpcForwarder.ready;
}
static register(serviceId, forwarder) {
// throw an error if the service has already been registered
if (RpcForwarder.forwardMap.has(serviceId)) {
const message = MsftSme.getStrings().MsftSmeShell.Core.Error.ForwarderIdConflict.message;
throw new Error(message.format(serviceId));
}
// register the forwarder using the serviceId
RpcForwarder.forwardMap.set(serviceId, forwarder);
}
static onForwardReceived(data) {
if (!RpcForwarder.forwardMap.has(data.service)) {
const message = MsftSme.getStrings().MsftSmeShell.Core.Error.ForwarderIdNotFound.message;
return Promise.reject(message.format(data.service));
}
else {
const target = RpcForwarder.forwardMap.get(data.service);
return target.handleForwardedMessage(data)
.pipe(map(result => ({ result: result })), catchError(error => {
// on error, make sure the error is serializable and return it as an observable
return of({ error: RpcForwarder.ensureSerializable(error) });
}))
.toPromise();
}
}
/**
* Transform object into something that can be serializable by dropping
* any non-serializable properties
*
* @param obj the object to make serializable.
* @return a new object that is serializable
*/
static ensureSerializable(obj) {
const type = typeof obj;
if (type === 'string' || type === 'number' || type === 'boolean') {
return obj;
}
if (obj && type === 'object') {
if (Array.isArray(obj)) {
return obj.map(v => RpcForwarder.ensureSerializable(v));
}
const keys = Object.keys(obj);
const result = {};
keys.forEach(key => {
result[key] = RpcForwarder.ensureSerializable(obj[key]);
});
return result;
}
// We will return null for symbol, function, null, undefined
return null;
}
}
/**
* Base class to allow 2+ instances of a service to behave as one across the iframe boundary
* three mechanisms are surfaced for communication:
* - Execute: expects a response with data from the receiver
* - Notify: expects a response with no data from the receiver just for confirmation that it was received
* - Init: always called from a child instance, used to synchronize parent and child as the child starts up
*/
export class RpcServiceForwarder {
serviceId;
rpc;
/**
* Instantiates a new instance of the RpcServiceForwarder
*/
constructor(serviceId, rpc) {
this.serviceId = serviceId;
this.rpc = rpc;
RpcForwarder.register(this.serviceId, this);
}
/**
* Called when a forwarded message is received from the rpc.
* @param data The RpcForwardReportData of the request
* @returns an observable for the result of the request call
*/
handleForwardedMessage(data) {
// in the future Rpc should have no knowledge of shell an module. Only child/parent.
const fromRpc = this.rpc.isShell ? 1 /* RpcRelationshipType.Child */ : 0 /* RpcRelationshipType.Parent */;
switch (data.type) {
case 2 /* RpcForwardType.Init */:
return this.onForwardInit();
case 0 /* RpcForwardType.Execute */:
return this.onForwardExecute(fromRpc, data.name, data.arguments);
case 1 /* RpcForwardType.Notify */:
return this.onForwardNotify(fromRpc, data.name, data.value);
default: {
const message = MsftSme.getStrings().MsftSmeShell.Core.Error.ForwarderUnknownType.message;
return throwError(() => message.format(data.type));
}
}
}
/**
* Initializes the service. The serviceReady observable is assigned to the output of this function.
*/
initialize() {
return RpcForwarder.waitForReady(this.rpc)
.pipe(mergeMap(() => {
if (!this.canForward(0 /* RpcRelationshipType.Parent */)) {
// since we dont have a parent we can stop here
return of(null);
}
// other wise we have a parent and should forward an init message
const data = {
service: this.serviceId,
type: 2 /* RpcForwardType.Init */
};
return from(RpcForwardAutoClient.forward(this.rpc, data));
}), map(result => result ? this.onForwardInitResponse(result) : null), map(() => true), take(1));
}
/**
* Reinitialize the forwarded service. This should poll any important data from parents.
*/
reinitializeForwardedData() {
if (!this.canForward(0 /* RpcRelationshipType.Parent */)) {
// since we dont have a parent we can stop here
return of(null);
}
// other wise we have a parent and should forward an init message
const data = {
service: this.serviceId,
type: 2 /* RpcForwardType.Init */
};
return from(RpcForwardAutoClient.forward(this.rpc, data))
.pipe(map(result => result ? this.onForwardInitResponse(result) : null));
}
/**
* Creates an observable that errors with name not found
* @returns an observable that will error with a name not found message
*/
nameNotFound(name) {
return throwError(() => new Error(`${name} not found in forwarded service: ${this.serviceId}`));
}
/**
* Determines if a message can be forwarded to the specified RpcRelationshipType
* @param to The RpcRelationshipType that needs to be checked
* @returns true if messages can be forwarded to the specified RpcRelationshipType
*/
canForward(to) {
// we can only forward if the rpc is active
if (this.rpc.stateActive) {
if (to === 0 /* RpcRelationshipType.Parent */) {
// we can only forward to parents if we are an extension window
return MsftSme.isExtension();
}
else if (to === 1 /* RpcRelationshipType.Child */) {
// when trying to forward to a child, we have to make sure that the rpc outbound is actually ready.
// rpc.StateActive will report true always fro the shell
if (!this.rpc.rpcManager || !this.rpc.rpcManager.rpcOutbound) {
return false;
}
// we can only forward to parents if we have child windows
return MsftSme.isShell() && window.frames.length > 0;
}
}
return false;
}
/**
* Forwards a execution of some named method to the target relationship type
* @param to The RpcRelationshipType that this request is intended for
* @param name The name of the method to execute
* @param value The arguments for the method
* @returns an observable for the result of the method call
*/
forwardExecute(to, name, args) {
// if we cant forward then just return
if (!this.canForward(to)) {
return;
}
const data = {
arguments: args,
name: name,
service: this.serviceId,
type: 0 /* RpcForwardType.Execute */
};
return from(RpcForwardAutoClient.forward(this.rpc, data))
.pipe(map(response => {
if (response.error) {
throw response.error;
}
else {
return response.result;
}
}));
}
/**
* Forwards a notification of some state change to the target relationship type
* @param to The RpcRelationshipType that this request is intended for
* @param name The name of the state change
* @param value The new value of some state
* @returns an observable that completes when the state has been changed on the target instance.
*/
forwardNotify(to, name, value, noPrimaryFrame = false) {
// if we cant forward then just return
if (!noPrimaryFrame && !this.canForward(to)) {
return;
}
const data = {
value: value,
name: name,
service: this.serviceId,
type: 1 /* RpcForwardType.Notify */
};
return from(RpcForwardAutoClient.forward(this.rpc, data))
.pipe(map(response => {
if (response.error) {
throw response.error;
}
else {
return response.result;
}
}));
}
}
//# sourceMappingURL=rpc-forwarder.js.map