diff --git a/src/background/requestHandler.js b/src/background/requestHandler.js index a36243fe..0ffe3d2d 100644 --- a/src/background/requestHandler.js +++ b/src/background/requestHandler.js @@ -42,11 +42,11 @@ export class RequestHandler extends Component { }); propertySum( - proxyHandler.localProxyInfo, - proxyHandler.currentExitRelays, (loophole, exitRelays) => { this.updateProxyInfoFromClient(loophole, exitRelays); - } + }, + proxyHandler.localProxyInfo, + proxyHandler.currentExitRelays ); proxyHandler.proxyMap.subscribe((proxyMap) => { diff --git a/src/shared/property.js b/src/shared/property.js index d37a97b2..4e4290a3 100644 --- a/src/shared/property.js +++ b/src/shared/property.js @@ -31,6 +31,34 @@ export class IBindable { } } +class BaseBindable extends IBindable { + /** @returns {T} */ + get value() { + return this.#innerValue; + } + /** + * + * @param {(T)=>void} callback - This callback will be called when the value changes + * @returns {()=>void} unsubscribe function, this will stop all callbacks + */ + subscribe(callback) { + const unsubscribe = () => { + this.#subscriptions = this.#subscriptions.filter((t) => t !== callback); + }; + this.#subscriptions.push(callback); + queueMicrotask(() => callback(this.#innerValue)); + return unsubscribe; + } + /** + * @type {Array<(arg0: T)=>void> } + */ + #subscriptions = []; + /** + * @type {T} + */ + #innerValue; +} + /** * A property similar to Q_Property * Holds an internal value that can be modified using `.set(NewValue)` @@ -40,7 +68,7 @@ export class IBindable { * * @template T */ -export class WritableProperty extends IBindable { +export class WritableProperty extends BaseBindable { /** * Constructs a Property with an initial Value * @param {T} initialvalue @@ -129,86 +157,6 @@ export class ReadOnlyProperty extends IBindable { #property; } -/** - * @template T - Internal Type - * @template P - Parent Property Type - * - * A property consuming another property - * and applying a transform function - * before emitting the new value - * - */ -class LazyComputedProperty { - /** - * Constructs a Bindable from a Property - * @param {WritableProperty

} parent - The proptery to read from - * @param {(arg0: (P|null) )=>T} transform - The function to apply - */ - constructor(parent, transform) { - this.#parent = parent; - this.#transform = transform; - this.#innerValue = this.#transform(this.#parent.value); - } - - get value() { - // If we're currently not subscribed, to the parent - // create the value on demand - if (!this.#parentUnsubscribe) { - return this.#transform(this.#parent.value); - } - // Otherwise innerValue is cached correctly - return this.#innerValue; - } - /** - * Subscribe to changes of the Value - * @param {(arg0: T)=>void} callback - A callback - */ - subscribe(callback) { - if (!this.#parentUnsubscribe) { - this.#parentUnsubscribe = this.#parent.subscribe((parentValue) => { - this.#notify(this.#transform(parentValue)); - }); - } - const unsubscribe = () => { - this.unsubscribe(callback); - }; - this.#subscriptions.push(callback); - return unsubscribe; - } - #notify(value) { - this.#innerValue = value; - this.#subscriptions.forEach((s) => s(value)); - } - unsubscribe(callback) { - this.#subscriptions = this.#subscriptions.filter((t) => t !== callback); - // Noone listens to us, no need to hook into the parent - if (this.#subscriptions.length == 0 && this.#parentUnsubscribe) { - this.#parentUnsubscribe(); - this.#parentUnsubscribe = null; - } - } - - /** - * @type {WritableProperty

} - * The Parent Property - */ - #parent; - /** @type { ?Function} */ - #parentUnsubscribe = null; - /** - * @type {(arg0: P?)=>T} - */ - #transform; - /** - * @type {Array<(arg0: T)=>void>} - */ - #subscriptions = []; - /** - * @type {T?} - */ - #innerValue = null; -} - /** * @template T * @param {T} value - Initial value of the Property @@ -221,12 +169,16 @@ export const property = (value) => { /** * @template T * @template P - * @param {WritableProperty

} property - Callback when the value changes + * @param {IBindable

} parent - Callback when the value changes * @param {(arg0: P?)=>T} transform - Called with the Property Value, must return the transformed value - * @returns {LazyComputedProperty} - A Function to stop the subscription + * @returns {ReadOnlyProperty} - A Function to stop the subscription */ -export const computed = (property, transform) => { - return new LazyComputedProperty(property, transform); +export const computed = (parent, transform) => { + const inner = new WritableProperty(transform(parent.value)); + parent.subscribe((value) => { + inner.set(transform(value)); + }); + return inner.readOnly; }; /** @@ -235,18 +187,33 @@ export const computed = (property, transform) => { * When either L or R changes calls the function * and updates the returned property. * + * Example: + * const prop1 = property("hello"); + * const prop2 = property(4); + * const prop3 = property(true); + * propertySum( (value1,value2,value3) => { + * expect(value1).toBe(prop1.value) + * expect(value2).toBe(prop2.value) + * expect(value3).toBe(prop3.value) + * return value1+value2+value3; + * }, prop1, prop2, prop3); * * @template T - * @template L - * @template R - * @param {IBindable} left - Left Hand Property - * @param {IBindable} right - Right Hand Property - * @param {(arg0: L, arg1: R)=>T} transform - Called with the Property Value, must return the transformed value - * @returns {ReadOnlyProperty} - + * @param {Array>} parent - Callback when the value changes + * @param {(...any)=>T} transform - Called with the Property Value, must return the transformed value + * @returns {ReadOnlyProperty} - A Function to stop the subscription */ -export const propertySum = (left, right, transform) => { - const inner = property(transform(left.value, right.value)); - left.subscribe((l) => inner.set(transform(l, right.value))); - right.subscribe((r) => inner.set(transform(left.value, r))); +export const propertySum = (transform, ...parent) => { + const getValues = () => { + return parent.map((p) => p.value); + }; + const inner = new WritableProperty(transform(...getValues())); + const onUpdated = () => { + const values = getValues(); + inner.set(transform(...getValues())); + }; + parent.forEach((p) => { + p.subscribe(onUpdated); + }); return inner.readOnly; }; diff --git a/src/ui/browserAction/popupConditional.js b/src/ui/browserAction/popupConditional.js index 7fadaa64..cfd6e99e 100644 --- a/src/ui/browserAction/popupConditional.js +++ b/src/ui/browserAction/popupConditional.js @@ -18,15 +18,15 @@ export class PopUpConditionalView extends ConditionalView { const supportedPlatform = Utils.isSupportedOs(deviceOs.os); propertySum( - vpnController.state, - vpnController.featureList, (state, features) => { this.slotName = PopUpConditionalView.toSlotname( state, features, supportedPlatform ); - } + }, + vpnController.state, + vpnController.featureList ); // Messages may dispatch an event requesting to send a Command to the VPN diff --git a/tests/jest/utils/property.test.mjs b/tests/jest/utils/property.test.mjs index 4f975c4c..e5d51dd1 100644 --- a/tests/jest/utils/property.test.mjs +++ b/tests/jest/utils/property.test.mjs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { describe, expect, test } from "@jest/globals"; -import { property, computed } from "../../../src/shared/property"; +import { property, computed, propertySum } from "../../../src/shared/property"; describe("property()", () => { test("Can create a property from a value", () => { @@ -80,3 +80,50 @@ describe("computed()", () => { expect(maybeValue).toBe(4); }); }); + +describe("propertySum", () => { + test("Can create a property from a value", () => { + const obj = property({ x: "hello" }); + const prop = propertySum((obj) => { + return obj.x; + }, obj); + expect(prop.value.x).toBe(obj.x); + }); + test("Calls the Transform Function in order of the props ", async () => { + const prop1 = property("hello"); + const prop2 = property(4); + const prop3 = property(true); + propertySum( + (value1, value2, value3) => { + expect(value1).toBe(prop1.value); + expect(value2).toBe(prop2.value); + expect(value3).toBe(prop3.value); + }, + prop1, + prop2, + prop3 + ); + }); + + test("If any of the props are updated, the transform is called", async () => { + const prop1 = property("hello"); + const prop2 = property(4); + const prop3 = property(true); + + let count = 0; + propertySum( + () => { + return count++; + }, + prop1, + prop2, + prop3 + ); + // We auto scheudle the transform to compute the inital value + expect(count).toBe(1); + prop1.set("h"); + prop2.set("h"); + prop3.set("h"); + expect(count).toBe(4); + }); +});