Skip to content

Commit

Permalink
Allow propertySum to consume any amount of properties
Browse files Browse the repository at this point in the history
  • Loading branch information
strseb committed Dec 11, 2024
1 parent 2c0ff4a commit a64475f
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 102 deletions.
6 changes: 3 additions & 3 deletions src/background/requestHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
157 changes: 62 additions & 95 deletions src/shared/property.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand All @@ -40,7 +68,7 @@ export class IBindable {
*
* @template T
*/
export class WritableProperty extends IBindable {
export class WritableProperty extends BaseBindable {
/**
* Constructs a Property<T> with an initial Value
* @param {T} initialvalue
Expand Down Expand Up @@ -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<T> from a Property
* @param {WritableProperty<P>} 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<P>}
* 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
Expand All @@ -221,12 +169,16 @@ export const property = (value) => {
/**
* @template T
* @template P
* @param {WritableProperty<P>} property - Callback when the value changes
* @param {IBindable<P>} parent - Callback when the value changes
* @param {(arg0: P?)=>T} transform - Called with the Property Value, must return the transformed value
* @returns {LazyComputedProperty<T,P>} - A Function to stop the subscription
* @returns {ReadOnlyProperty<T>} - 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;
};

/**
Expand All @@ -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<L>} left - Left Hand Property
* @param {IBindable<R>} right - Right Hand Property
* @param {(arg0: L, arg1: R)=>T} transform - Called with the Property Value, must return the transformed value
* @returns {ReadOnlyProperty<T>} -
* @param {Array<IBindable<any>>} parent - Callback when the value changes
* @param {(...any)=>T} transform - Called with the Property Value, must return the transformed value
* @returns {ReadOnlyProperty<T>} - 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;
};
6 changes: 3 additions & 3 deletions src/ui/browserAction/popupConditional.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 48 additions & 1 deletion tests/jest/utils/property.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
});
});

0 comments on commit a64475f

Please sign in to comment.