blob: c9b38a98b0606b4ae5b9cc7950b6b68468e53d8c [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Note: this code is made to conform to glic_api's Observable, but can also be
// used independently.
/** Allows control of a subscription to an ObservableValue. */
export declare interface Subscriber {
unsubscribe(): void;
}
class ObservableSubscription<T> implements Subscriber {
constructor(
public onChange: (newValue: T) => void,
private onUnsubscribe: (self: ObservableSubscription<T>) => void) {}
unsubscribe(): void {
this.onUnsubscribe(this);
}
}
export interface ObservableValueReadOnly<T> {
getCurrentValue(): T|undefined;
subscribe(change: (newValue: T) => void): Subscriber;
}
/**
* A observable value that can change over time. If value is initialized, sends
* it to new subscribers upon subscribe().
*/
export class ObservableValue<T> {
private subscribers: Set<ObservableSubscription<T>> = new Set();
private constructor(private isSet: boolean, private value: T|undefined) {}
/** Create an ObservableValue which has an initial value. */
static withValue<T>(value: T) {
return new ObservableValue(true, value);
}
/**
* Create an ObservableValue which has no initial value. Subscribers will not
* be called until after assignAndSignal() is called the first time.
*/
static withNoValue<T>() {
return new ObservableValue<T>(false, undefined);
}
assignAndSignal(v: T, force = false) {
const send = !this.isSet || this.value !== v || force;
this.isSet = true;
this.value = v;
if (!send) {
return;
}
this.subscribers.forEach((sub) => {
// Ignore if removed since forEach was called.
if (this.subscribers.has(sub)) {
try {
sub.onChange(v);
} catch (e) {
console.warn(e);
}
}
});
}
/** Returns the current value, or undefined if not initialized. */
getCurrentValue(): T|undefined {
return this.value;
}
/** Receive updates for value changes. */
subscribe(change: (newValue: T) => void): Subscriber {
const newSub = new ObservableSubscription(
change, (sub) => this.subscribers.delete(sub));
this.subscribers.add(newSub);
if (this.isSet) {
change(this.value!);
}
return newSub;
}
}