Skip to main content

Specialized Subscribables

MappedSubject

This section on subscribables discusses how a subscribable can be mapped to a new subscribable via the map() method. While map() works for mapping a single input to an output, it can't handle mapping multiple inputs to an output. For the multiple-input case, we can use MappedSubject instead.

Let's see how we can use MappedSubject to map a subscribable that provides the current headwind component from multiple inputs:

import { MappedSubject, MathUtils } from '@microsoft/msfs-sdk';

const planeHeading: Subscribable<number> = ...;
const windDirection: Subscribable<number> = ...;
const windSpeed: Subscribable<number> = ...;

const headwind = MappedSubject.create(
([planeHeading, windDirection, windSpeed]) => {
const relativeWindAngle = MathUtils.diffAngleDeg(planeHeading, windDirection);
return windSpeed * Math.cos(relativeWindAngle * Math.PI / 180);
},
this.planeHeading,
this.windDirection,
this.windSpeed
);

In the above example, MappedSubject.create() takes a mapping function and an ordered sequence of input subscribables and returns a new MappedSubject (which implements MappedSubscribable) whose value is the result of applying the mapping function to the values of the input subscribables. The values of the input subscribables are provided to the mapping function as a parameter in the form of an ordered readonly tuple. In the above example the input parameter has a type of readonly [number, number, number] and is destructured into the planeHeading, windDirection, and windSpeed components.

MappedSubject supports any number of inputs (including zero!). You don't even need to know the exact number of inputs at compile time by taking advantage of the spread operator:

const inputArray: Subscribable<number>[] = ...;

// Maps n inputs, where n is in the range [0, ∞), to their sum.
const sum = MappedSubject.create(
inputs => inputs.reduce((sum, value) => sum + value, 0),
...inputArray
);
note

The mapping function used to create a MappedSubject is executed whenever any of the inputs change. For mappings with many inputs that frequently change, the performance cost of running the mapping function should be taken into account when designing it.

You can choose to omit the mapping function from MappedSubject.create(), in which case the inputs will be mapped to an ordered readonly n-tuple (the same tuple that would be passed into the mapping function). This can be useful if a handler needs to be called whenever any one of multiple values changes:

const a: Subscribable<number> = ...;
const b: Subscribable<boolean> = ...;
const c: Subscribable<string> = ...;

// Instead of...

const handler = (): void => {
const aVal = a.get();
const bVal = b.get();
const cVal = c.get();

// Do something...
}

a.sub(handler);
b.sub(handler);
c.sub(handler);

// ... We can do...

MappedSubject.create(a, b, c).sub(([aVal, bVal, cVal]) => {
// Do something...
});

When MappedSubject is used to map inputs to an n-tuple, the mapped n-tuple is considered to have changed (and therefore will trigger notifications to subscribers) whenever any of the inputs change. For MappedSubjects created with a mapping function, the standard strict equality (===) logic is used to compare values unless a custom equality function is defined:

const inputArray: Subscribable<number>[] = ...;

const sum = MappedSubject.create(
inputs => inputs.reduce((sum, value) => sum + value, 0),
// Custom equality function that ensures NaN is considered equal to itself.
(a, b) => {
if (isNaN(a) && isNaN(b)) {
return true;
} else {
return a === b;
}
},
...inputArray
);
note

The equality function used to create a MappedSubject compares mapped values, not input values.

Finally, because MappedSubject implements MappedSubscribable and Subscription, it supports the standard pause, resume, and destroy operations. These operations function in the same manner as described for mapped subscribables returned by map().

ObjectSubject

ObjectSubject is a subscribable with an object value whose enumerable properties represent a set of key-value pairs. Once defined, properties (keys) can't be added or removed from the object. The object value is considered to change when the value of any of its properties changes. Property values are compared using the strict equality operator (===).

ObjectSubject allows in-place mutation of its object value on a per-property basis or using a syntax similar to Object.assign(). Handlers subscribed to an ObjectSubject via sub() are notified of changes to each individual property:

import { ObjectSubject } from '@microsoft/msfs-sdk';

const subject = ObjectSubject.create({
'prop1': 0,
'prop2': false,
'prop3': ''
});

subject.sub((obj, prop, value) => {
console.log(`Property ${prop} changed to ${value}`);
});

// 'Property prop1 changed to 5' is logged to the console.
subject.set('prop1', 5);

// 'Property prop2 changed to false' is logged to the console.
subject.set('prop2', true);

// 'Property prop1 changed to 10', then 'Property prop3 changed to qwerty'
// are logged to the console.
subject.set({ 'prop1': 10, 'prop3': 'qwerty' });
caution

Mutating the object value of an ObjectSubject through means other than ObjectSubject's set() method will not trigger notifications to subscribers:

const obj = { 'prop1': 0 }
const subject = ObjectSubject.create(obj);

// None of the following will trigger notifications:
obj.prop1 = 1;
obj['prop1'] = 5;
Object.assign(obj, { 'prop1': 10 });

To help prevent errors like the above, ObjectSubject only provides a readonly version of its object value through the get() method and to subscribers.

ArraySubject

ArraySubject is a subscribable whose value is an array. Importantly, ArraySubject does not implement the Subscribable interface. Instead, it implements the SubscribableArray interface.

ArraySubject allows in-place mutation of its array value in various ways, including insertion and deletion of indexes and changing the value stored at an index. Handlers subscribed to an ArraySubject via sub() are notified of changes in the array and provided information on which indexes changed:

import { ArraySubject, SubscribableArrayEventType } from '@microsoft/msfs-sdk';

// The subject's value is initialized to an empty array.
const subject = ArraySubject.create<number>();

subject.sub((index: number, type: SubscribableArrayEventType, items: number | number[], array: readonly number[]) => {
if (type === SubscribableArrayEventType.Cleared) {
console.log('Array was cleared');
} else {
console.log(`${type === SubscribableArrayEventType.Added ? 'Added' : 'Removed'} at index ${index}: ${items}`);
}
});

subject.insert(1); // 'Added at index 0: 1' is logged to the console.
subject.insertRange(1, [2, 3, 4]); // 'Added at index 1: [2, 3, 4]' is logged to the console.
console.log(subject.getArray()); // '[1, 2, 3, 4]' is logged to the console.

subject.removeAt(2); // 'Removed at index 2: 3' is logged to the console.
console.log(subject.getArray()); // '[1, 2, 4]' is logged to the console.

// 'Array was cleared', then 'Added at index 0: [4, 3, 2, 1]' are logged to the console.
subject.set([4, 3, 2, 1]);
caution

Mutating the array value of an ArraySubject through means other than ArraySubject's built-in methods will not trigger notifications to subscribers:

const arr: number[] = [];
const subject = ArraySubject.create(arr);

// None of the following will trigger notifications:
arr.push(0);
arr.splice(0, 1);
arr[0] = 5;

To help prevent errors like the above, ArraySubject only provides a readonly version of its array value through the getArray() method and to subscribers.

SetSubject

SetSubject is a subscribable whose value is a Javascript Set object. A set is a collection of keys in which each key appears at most once. Keys can be freely added to and removed from a SetSubject using syntax that mirrors the one used by Set. Subscribers are notified of each key that is added or removed.

import { SetSubject, SubscribableSetEventType } from '@microsoft/msfs-sdk';

// Creates a new SetSubject whose initial keys include the numbers 1, 2, 3.
const subject = SetSubject.create<number>([1, 2, 3]);

subject.sub((set: ReadonlySet<number>, type: SubscribableSetEventType, key: number) => {
console.log(`Key ${key} was ${type === SubscribableSetEventType.Added ? 'added' : 'removed'}`);
});

// 'Key 4 was added' is logged to the console.
subject.add(4);

// 'Key 1 was removed' is logged to the console.
subject.delete(1);

// Nothing is logged to the console (the set remains unchanged).
subject.add(4);

// 'Key 0 was added' is logged to the console.
subject.toggle(0);

// 'Key 0 was removed' is logged to the console.
subject.toggle(0);

// '[2, 3, 4]' is logged to the console.
console.log([...subject.get()]);

// 'Key 4 was removed', then 'Key 1 was added' are logged to the console.
subject.set([1, 2, 3]);
caution

Mutating the set value of a SetSubject through means other than SetSubject's built-in methods will not trigger notifications to subscribers. SetSubject only provides a readonly version of its set value (ReadonlySet) through the get() method and to subscribers in order to prevent code from changing the set in inappropriate ways. As long as the value is not explicitly cast to the mutable version (Set), Typescript should protect you from unintentionally changing it outside of SetSubject.

Additionally, if you pass an existing Set into SetSubject.create(), the passed-in set is copied into a newly created set. Therefore, any further changes to the passed-in set will not be reflected in the new SetSubject's value.

In addition to the standard Subscribable pipe(), SetSubject supports piping individual keys into objects implementing the MutableSubscribableSet interface (this includes SetSubject itself). What distinguishes this set-specific pipe from the more general version is the ability to transform keys through the pipe. For example, we can use this feature to maintain a set containing keys that are the string representations of the values of another set:

const numbers = SetSubject.create<number>([1, 2, 3]);
const strings = SetSubject.create<string>();

numbers.pipe(strings, key => key.toString());
console.log([...strings.get()]); // '["1", "2", "3"]'

numbers.delete(2);
console.log([...strings.get()]); // '["1", "3"]'

numbers.add(0);
console.log([...strings.get()]); // '["1", "3", "0"]'

When establishing set pipes that do not use an injective ("one-to-one") transform function, removing a key from the source set removes the transformed key in the target set if and only if there does not exist another key in the source set that maps to the same transformed key:

const numbers = SetSubject.create<number>([1.1, 1.2, 3.5, 5]);
const rounded = SetSubject.create<number>();

numbers.pipe(rounded, Math.round);
console.log([...rounded.get()]); // '[1, 4, 5]'

numbers.delete(3.5);
console.log([...rounded.get()]); // '[1, 5]'

numbers.delete(1.1);
console.log([...rounded.get()]); // '[1, 5]'

numbers.delete(1.2);
console.log([...rounded.get()]); // '[5]'

MapSubject

MapSubject is a subscribable whose value is a Javascript Map object. A map is a collection of key-value pairs in which each key appears at most once. Key-value pairs can be freely manipulated (adding/removing keys, changing the value of a key) in MapSubject using syntax that mirrors the one used by Map. Subscribers are notified of each key-value pair that is changed.

import { MapSubject, SubscribableMapEventType } from '@microsoft/msfs-sdk';

// Creates a new MapSubject whose initial key-value pairs are: 'a': 1, 'b': 2, and 'c': 3.
const subject = MapSubject.create<string, number>([['a', 1], ['b', 2], ['c', 3]]);

subject.sub((map: ReadonlyMap<string, number>, type: SubscribableMapEventType, key: string, value: number) => {
switch (type) {
case SubscribableMapEventType.Added:
console.log(`Key ${key} was added with value ${value}`);
case SubscribableMapEventType.Changed:
console.log(`Key ${key} was changed with value ${value}`);
case SubscribableMapEventType.Deleted:
console.log(`Key ${key} was removed with value ${value}`);
}
});

// 'Key d was added with value 4' is logged to the console.
subject.setValue('d', 4);

// 'Key a was removed with value 1' is logged to the console.
subject.delete('a');

// 'Key b was changed with value 5' is logged to the console.
subject.setValue('b', 5);

// '[["b", 5], ["c", 3], ["d", 4]]' is logged to the console.
console.log([...subject.get()]);

// 'Key b was removed with value 5', then 'Key c was changed with value 0', then 'Key e was added with value 5'
// are logged to the console.
subject.set([['c', 0], ['e', 5]]);
caution

Mutating the map value of a MapSubject through means other than MapSubject's built-in methods will not trigger notifications to subscribers. MapSubject only provides a readonly version of its map value (ReadonlyMap) through the get() method and to subscribers in order to prevent code from changing the map in inappropriate ways. As long as the value is not explicitly cast to the mutable version (Map), Typescript should protect you from unintentionally changing it outside of MapSubject.

Additionally, if you pass an existing Map into MapSubject.create(), the passed-in map is copied into a newly created map. Therefore, any further changes to the passed-in map will not be reflected in the new MapSubject's value.