forked from MapComplete/MapComplete
Performance: sprinkle 'onDestroy' everywhere to cleanup old stores; cleanup 'Stores' utility class
This commit is contained in:
parent
66f093afd8
commit
81be4db044
79 changed files with 332 additions and 325 deletions
|
|
@ -47,7 +47,7 @@ export default class TileLocalStorage<T> {
|
|||
return cached
|
||||
}
|
||||
const src = <UIEventSource<T> & { flush: () => void }>(
|
||||
UIEventSource.FromPromise(this.GetIdb(tileIndex))
|
||||
UIEventSource.fromPromise(this.GetIdb(tileIndex))
|
||||
)
|
||||
src.flush = () => this.SetIdb(tileIndex, src.data)
|
||||
src.addCallbackD((data) => this.SetIdb(tileIndex, data))
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default class ChangeGeometryApplicator implements FeatureSource {
|
|||
|
||||
source.features.addCallbackAndRunD(() => this.update())
|
||||
|
||||
Stores.ListStabilized(changes.allChanges).addCallbackAndRunD(() => this.update())
|
||||
Stores.listStabilized(changes.allChanges).addCallbackAndRunD(() => this.update())
|
||||
}
|
||||
|
||||
private update() {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
|
|||
public readonly allFavourites: Store<Feature[]>
|
||||
|
||||
constructor(state: WithChangesState) {
|
||||
const features: Store<Feature[]> = Stores.ListStabilized(
|
||||
const features: Store<Feature[]> = Stores.listStabilized(
|
||||
state.osmConnection.preferencesHandler.allPreferences.map((prefs) => {
|
||||
const feats: Feature[] = []
|
||||
const allIds = new Set<string>()
|
||||
|
|
@ -60,7 +60,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
|
|||
this.allFavourites = features
|
||||
|
||||
this._osmConnection = state.osmConnection
|
||||
this._detectedIds = Stores.ListStabilized(
|
||||
this._detectedIds = Stores.listStabilized(
|
||||
features.map((feats) => feats.map((f) => f.properties.id))
|
||||
)
|
||||
const allFeatures = state.indexedFeatures
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export default class NearbyFeatureSource implements FeatureSource {
|
|||
this._currentZoom = options?.currentZoom.stabilized(500)
|
||||
this._bounds = options?.bounds
|
||||
|
||||
this.features = Stores.ListStabilized(this._result)
|
||||
this.features = Stores.listStabilized(this._result)
|
||||
|
||||
sources.forEach((source, layer) => {
|
||||
this.registerSource(source, layer)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export default class DynamicTileSource<
|
|||
this.zDiff = options?.zDiff ?? 0
|
||||
this.bounds = mapProperties.bounds
|
||||
|
||||
const neededTiles: Store<number[]> = Stores.ListStabilized(
|
||||
const neededTiles: Store<number[]> = Stores.listStabilized(
|
||||
mapProperties.bounds
|
||||
.mapD(() => {
|
||||
if (options?.isActive && !options?.isActive.data) {
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export class SummaryTileSource extends DynamicTileSource {
|
|||
const [z, x, y] = Tiles.tile_from_index(tileIndex)
|
||||
let coordinates = Tiles.centerPointOf(z, x, y)
|
||||
const url = `${cacheserver}/summary/${layersSummed}/${z}/${x}/${y}.json`
|
||||
const count = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url))
|
||||
const count = UIEventSource.fromPromiseWithErr(Utils.downloadJson(url))
|
||||
return count.mapD((count) => {
|
||||
if (count["error"] !== undefined) {
|
||||
console.error(
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export default class AllImageProviders {
|
|||
const singleSource = tags.bindD((tags) => imageProvider.getRelevantUrls(tags, prefixes))
|
||||
allSources.push(singleSource)
|
||||
}
|
||||
const source = Stores.fromStoresArray(allSources).map((result) => {
|
||||
const source = Stores.concat(allSources).map((result) => {
|
||||
const all = Utils.concat(result)
|
||||
return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Store, Stores } from "../UIEventSource"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { LicenseInfo } from "./LicenseInfo"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Feature, Point } from "geojson"
|
||||
|
|
@ -114,7 +114,7 @@ export default abstract class ImageProvider {
|
|||
tags: Record<string, string>,
|
||||
prefixes: string[]
|
||||
): Store<ProvidedImage[]> {
|
||||
return Stores.FromPromise(this.getRelevantUrlsFor(tags, prefixes))
|
||||
return UIEventSource.fromPromise(this.getRelevantUrlsFor(tags, prefixes))
|
||||
}
|
||||
|
||||
public abstract ExtractUrls(
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export class ImageUploadManager {
|
|||
this._changes = changes
|
||||
this._gps = gpsLocation
|
||||
this._reportError = reportError
|
||||
Stores.Chronic(5 * 60000).addCallback(() => {
|
||||
Stores.chronic(5 * 60000).addCallback(() => {
|
||||
// If images failed to upload: attempt to reupload
|
||||
this.uploadQueue()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ export class Mapillary extends ImageProvider {
|
|||
*/
|
||||
public static isInStrictMode(): Store<boolean> {
|
||||
if (this._isInStrictMode === undefined) {
|
||||
this._isInStrictMode = UIEventSource.FromPromise(this.checkStrictMode())
|
||||
this._isInStrictMode = UIEventSource.fromPromise(this.checkStrictMode())
|
||||
}
|
||||
return this._isInStrictMode
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
}
|
||||
|
||||
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
|
||||
const source = UIEventSource.FromPromise(super.getRelevantUrlsFor(tags, prefixes))
|
||||
const source = UIEventSource.fromPromise(super.getRelevantUrlsFor(tags, prefixes))
|
||||
function hasLoading(data: ProvidedImage[]) {
|
||||
if (data === undefined) {
|
||||
return true
|
||||
|
|
@ -182,14 +182,14 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
)
|
||||
}
|
||||
|
||||
Stores.Chronic(5000, () => hasLoading(source.data)).addCallback(() => {
|
||||
Stores.chronic(5000, () => hasLoading(source.data)).addCallback(() => {
|
||||
super.getRelevantUrlsFor(tags, prefixes).then((data) => {
|
||||
source.set(data)
|
||||
return !hasLoading(data)
|
||||
})
|
||||
})
|
||||
|
||||
return Stores.ListStabilized(source)
|
||||
return Stores.listStabilized(source)
|
||||
}
|
||||
|
||||
public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> {
|
||||
|
|
|
|||
|
|
@ -682,7 +682,7 @@ export class OsmConnection {
|
|||
if (this.isChecking) {
|
||||
return
|
||||
}
|
||||
Stores.Chronic(3 * 1000).addCallback(() => {
|
||||
Stores.chronic(3 * 1000).addCallback(() => {
|
||||
if (!(this.apiIsOnline.data === "unreachable" || this.apiIsOnline.data === "offline")) {
|
||||
return
|
||||
}
|
||||
|
|
@ -699,7 +699,7 @@ export class OsmConnection {
|
|||
if (!this._doCheckRegularly) {
|
||||
return
|
||||
}
|
||||
Stores.Chronic(60 * 5 * 1000).addCallback(() => {
|
||||
Stores.chronic(60 * 5 * 1000).addCallback(() => {
|
||||
// Check for new messages every 5 minutes
|
||||
if (this.isLoggedIn.data) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,6 @@ export class NominatimGeocoding implements GeocodingProvider {
|
|||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] } | { error: any }> {
|
||||
return UIEventSource.FromPromiseWithErr(this.search(query, options))
|
||||
return UIEventSource.fromPromiseWithErr(this.search(query, options))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,6 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
|
||||
return UIEventSource.FromPromiseWithErr(this.search(query, options))
|
||||
return UIEventSource.fromPromiseWithErr(this.search(query, options))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Utils } from "../../Utils"
|
|||
import { Feature, FeatureCollection } from "geojson"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { Store, Stores } from "../UIEventSource"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
|
||||
export default class PhotonSearch implements GeocodingProvider, ReverseGeocodingProvider {
|
||||
private readonly _endpoint: string
|
||||
|
|
@ -74,7 +74,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
|
||||
return Stores.FromPromiseWithErr(this.search(query, options))
|
||||
return UIEventSource.fromPromiseWithErr(this.search(query, options))
|
||||
}
|
||||
|
||||
private buildDescription(entry: Feature) {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export class ThemeSearchIndex {
|
|||
theme: ThemeConfig
|
||||
}): Store<ThemeSearchIndex> {
|
||||
const layersToIgnore = state.theme.layers.filter((l) => l.isNormal()).map((l) => l.id)
|
||||
const knownHidden: Store<string[]> = Stores.ListStabilized(
|
||||
const knownHidden: Store<string[]> = Stores.listStabilized(
|
||||
UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection)
|
||||
.stabilized(1000)
|
||||
.map((list) => Utils.Dedup(list))
|
||||
|
|
|
|||
|
|
@ -53,8 +53,13 @@ class RoundRobinStore<T> {
|
|||
* @param t
|
||||
*/
|
||||
public add(t: T) {
|
||||
const i = this._index.data
|
||||
this._index.set((i + 1) % this._maxCount)
|
||||
const i = this._index.data ?? 0
|
||||
if(isNaN(Number(i))){
|
||||
this._index.set(0)
|
||||
this.add(t)
|
||||
return
|
||||
}
|
||||
this._index.set((Math.max(i,0) + 1) % this._maxCount)
|
||||
this._store.data[i] = t
|
||||
this._store.ping()
|
||||
}
|
||||
|
|
@ -84,19 +89,17 @@ export class OptionallySyncedHistory<T extends object | string> {
|
|||
defaultValue: "sync",
|
||||
})
|
||||
|
||||
this.syncedBackingStore = Stores.fromArray(
|
||||
Utils.TimesT(maxHistory, (i) => {
|
||||
const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
|
||||
return UIEventSource.asObject<T>(pref, undefined)
|
||||
})
|
||||
)
|
||||
this.syncedBackingStore = UIEventSource.concat(Utils.TimesT(maxHistory, (i) => {
|
||||
const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
|
||||
return UIEventSource.asObject<T>(pref, undefined)
|
||||
}))
|
||||
|
||||
const ringIndex = UIEventSource.asInt(
|
||||
osmconnection.getPreference(key + "-hist-round-robin", {
|
||||
defaultValue: "0",
|
||||
})
|
||||
)
|
||||
this.syncedOrdered = new RoundRobinStore<T>(this.syncedBackingStore, ringIndex, 10)
|
||||
this.syncedOrdered = new RoundRobinStore<T>(this.syncedBackingStore, ringIndex, maxHistory)
|
||||
const local = (this.local = LocalStorageSource.getParsed<T[]>(key + "-history", []))
|
||||
const thisSession = (this.thisSession = new UIEventSource<T[]>(
|
||||
[],
|
||||
|
|
@ -170,10 +173,10 @@ export class OptionallySyncedHistory<T extends object | string> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds the value when the user is actually logged in
|
||||
* Sets the value when the user is actually logged in
|
||||
* @param t
|
||||
*/
|
||||
public addDefferred(t: T) {
|
||||
public addDeferred(t: T) {
|
||||
if (t === undefined) {
|
||||
return
|
||||
}
|
||||
|
|
@ -217,7 +220,7 @@ export default class UserRelatedState {
|
|||
|
||||
public readonly fixateNorth: UIEventSource<undefined | "yes">
|
||||
public readonly a11y: UIEventSource<undefined | "always" | "never" | "default">
|
||||
public readonly homeLocation: FeatureSource
|
||||
public readonly homeLocation: FeatureSource<Feature>
|
||||
public readonly morePrivacy: UIEventSource<undefined | "yes" | "no">
|
||||
/**
|
||||
* The language as saved into the preferences of the user, if logged in.
|
||||
|
|
@ -335,7 +338,7 @@ export default class UserRelatedState {
|
|||
(a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type
|
||||
)
|
||||
this.syncLanguage()
|
||||
this.recentlyVisitedThemes.addDefferred(layout?.id)
|
||||
this.recentlyVisitedThemes.addDeferred(layout?.id)
|
||||
}
|
||||
|
||||
private syncLanguage() {
|
||||
|
|
@ -461,9 +464,9 @@ export default class UserRelatedState {
|
|||
)
|
||||
}
|
||||
|
||||
private initHomeLocation(): FeatureSource {
|
||||
private initHomeLocation(): FeatureSource<Feature> {
|
||||
const empty = []
|
||||
const feature: Store<Feature[]> = Stores.ListStabilized(
|
||||
const feature: Store<Feature[]> = Stores.listStabilized(
|
||||
this.osmConnection.userDetails.map((userDetails) => {
|
||||
if (userDetails === undefined) {
|
||||
return undefined
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Readable, Subscriber, Unsubscriber, Updater, Writable } from "svelte/st
|
|||
* Various static utils
|
||||
*/
|
||||
export class Stores {
|
||||
public static Chronic(millis: number, asLong: () => boolean = undefined): Store<Date> {
|
||||
public static chronic(millis: number, asLong: () => boolean = undefined): Store<Date> {
|
||||
const source = new UIEventSource<Date>(undefined)
|
||||
|
||||
function run() {
|
||||
|
|
@ -23,32 +23,12 @@ export class Stores {
|
|||
return source
|
||||
}
|
||||
|
||||
public static FromPromiseWithErr<T>(
|
||||
promise: Promise<T>
|
||||
): Store<{ success: T } | { error: any }> {
|
||||
return UIEventSource.FromPromiseWithErr(promise)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated.
|
||||
* If the promise fails, the value will stay undefined
|
||||
* @param promise
|
||||
* @constructor
|
||||
*/
|
||||
public static FromPromise<T>(promise: Promise<T>): Store<T | undefined> {
|
||||
const src = new UIEventSource<T>(undefined)
|
||||
promise
|
||||
?.catch((err): undefined => {
|
||||
console.warn("Promise failed:", err)
|
||||
return undefined
|
||||
})
|
||||
?.then((d) => src.setData(d))
|
||||
return src
|
||||
}
|
||||
public static concat<T>(stores: Store<T | undefined>[]): Store<(T | undefined)[]> ;
|
||||
public static concat<T>(stores: Store<T>[]): Store<T[]> ;
|
||||
public static concat<T>(stores: Store<T | undefined>[]): Store<(T | undefined)[]> {
|
||||
const newStore = new UIEventSource<(T | undefined)[]>([])
|
||||
public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]> ;
|
||||
public static concat<T>(stores: ReadonlyArray<Store<T>>): Store<T[]> ;
|
||||
public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]> {
|
||||
const newStore = new UIEventSource<(T | undefined)[]>(
|
||||
stores.map(store => store?.data),
|
||||
)
|
||||
|
||||
function update() {
|
||||
if (newStore._callbacks.isDestroyed) {
|
||||
|
|
@ -64,7 +44,6 @@ export class Stores {
|
|||
for (const store of stores) {
|
||||
store.addCallback(() => update())
|
||||
}
|
||||
update()
|
||||
return newStore
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +61,7 @@ export class Stores {
|
|||
* @param src
|
||||
* @constructor
|
||||
*/
|
||||
public static ListStabilized<T>(src: Store<T[]>): Store<T[]> {
|
||||
public static listStabilized<T>(src: Store<T[]>): Store<T[]> {
|
||||
const stable = new UIEventSource<T[]>(undefined)
|
||||
src.addCallbackAndRun((list) => {
|
||||
if (list === undefined) {
|
||||
|
|
@ -110,33 +89,6 @@ export class Stores {
|
|||
})
|
||||
return newStore
|
||||
}
|
||||
|
||||
public static fromArray<T>(sources: ReadonlyArray<UIEventSource<T>>): UIEventSource<T[]> {
|
||||
const src = new UIEventSource<T[]>(sources.map((s) => s.data))
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
sources[i].addCallback((content) => {
|
||||
src.data[i] = content
|
||||
src.ping()
|
||||
})
|
||||
}
|
||||
src.addCallbackD((contents) => {
|
||||
for (let i = 0; i < contents.length; i++) {
|
||||
sources[i]?.setData(contents[i])
|
||||
}
|
||||
})
|
||||
return src
|
||||
}
|
||||
|
||||
public static fromStoresArray<T>(sources: ReadonlyArray<Store<T>>): Store<T[]> {
|
||||
const src = new UIEventSource<T[]>(sources.map((s) => s.data))
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
sources[i].addCallback((content) => {
|
||||
src.data[i] = content
|
||||
src.ping()
|
||||
})
|
||||
}
|
||||
return src
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Store<T> implements Readable<T> {
|
||||
|
|
@ -162,17 +114,26 @@ export abstract class Store<T> implements Readable<T> {
|
|||
}
|
||||
|
||||
abstract map<J>(f: (t: T) => J): Store<J>
|
||||
abstract map<J>(f: (t: T) => J, extraStoresToWatch: Store<unknown>[]): Store<J>
|
||||
abstract map<J>(f: (t: T) => J, callbackDestroyFunction: (f: () => void) => void): Store<J>
|
||||
abstract map<J>(
|
||||
f: (t: T) => J,
|
||||
extraStoresToWatch: Store<unknown>[],
|
||||
callbackDestroyFunction: (f: () => void) => void
|
||||
callbackDestroyFunction?: (f: () => void) => void,
|
||||
): Store<J>
|
||||
|
||||
public mapD<J>(
|
||||
f: (t: Exclude<T, undefined | null>) => J,
|
||||
extraStoresToWatch?: Store<unknown>[],
|
||||
callbackDestroyFunction?: (f: () => void) => void
|
||||
callbackDestroyFunction?: (f: () => void) => void,
|
||||
): Store<J>
|
||||
public mapD<J>(
|
||||
f: (t: Exclude<T, undefined | null>) => J,
|
||||
callbackDestroyFunction?: (f: () => void) => void,
|
||||
): Store<J>
|
||||
public mapD<J>(
|
||||
f: (t: Exclude<T, undefined | null>) => J,
|
||||
extraStoresToWatch?: Store<unknown>[] | ((f: () => void) => void),
|
||||
callbackDestroyFunction?: (f: () => void) => void,
|
||||
): Store<J> {
|
||||
return this.map(
|
||||
(t) => {
|
||||
|
|
@ -184,8 +145,8 @@ export abstract class Store<T> implements Readable<T> {
|
|||
}
|
||||
return f(<Exclude<T, undefined | null>>t)
|
||||
},
|
||||
extraStoresToWatch,
|
||||
callbackDestroyFunction
|
||||
typeof extraStoresToWatch === "function" ? [] : extraStoresToWatch,
|
||||
callbackDestroyFunction ?? (typeof extraStoresToWatch === "function" ? extraStoresToWatch : undefined),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -212,22 +173,6 @@ export abstract class Store<T> implements Readable<T> {
|
|||
*/
|
||||
abstract addCallbackAndRun(callback: (data: T) => void): () => void
|
||||
|
||||
public withEqualityStabilized(
|
||||
comparator: (t: T | undefined, t1: T | undefined) => boolean
|
||||
): Store<T> {
|
||||
let oldValue = undefined
|
||||
return this.map((v) => {
|
||||
if (v == oldValue) {
|
||||
return oldValue
|
||||
}
|
||||
if (comparator(oldValue, v)) {
|
||||
return oldValue
|
||||
}
|
||||
oldValue = v
|
||||
return v
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Monadic bind function
|
||||
*
|
||||
|
|
@ -274,8 +219,9 @@ export abstract class Store<T> implements Readable<T> {
|
|||
* src.setData(0)
|
||||
* lastValue // => "def"
|
||||
*/
|
||||
public bind<X>(f: (t: T) => Store<X>, extraSources: Store<unknown>[] = []): Store<X> {
|
||||
const mapped = this.map(f, extraSources)
|
||||
public bind<X>(f: (t: T) => Store<X>, extraSources?: Store<unknown>[] | ((f: () => void) => void), onDestroy?: (f : () => void) => void): Store<X> {
|
||||
const mapped = this.map(f, typeof extraSources === "function" ? undefined : extraSources,
|
||||
onDestroy ?? (typeof extraSources === "function" ? extraSources : undefined))
|
||||
const sink = new UIEventSource<X>(undefined)
|
||||
const seenEventSources = new Set<Store<X>>()
|
||||
mapped.addCallbackAndRun((newEventSource) => {
|
||||
|
|
@ -308,7 +254,8 @@ export abstract class Store<T> implements Readable<T> {
|
|||
|
||||
public bindD<X>(
|
||||
f: (t: Exclude<T, undefined | null>) => Store<X>,
|
||||
extraSources: Store<unknown>[] = []
|
||||
extraSources?: Store<unknown>[],
|
||||
onDestroy?: ((f: () => void) => void)
|
||||
): Store<X> {
|
||||
return this.bind((t) => {
|
||||
if (t === null) {
|
||||
|
|
@ -318,7 +265,7 @@ export abstract class Store<T> implements Readable<T> {
|
|||
return undefined
|
||||
}
|
||||
return f(<Exclude<T, undefined | null>>t)
|
||||
}, extraSources)
|
||||
}, extraSources, onDestroy)
|
||||
}
|
||||
|
||||
public stabilized(millisToStabilize): Store<T> {
|
||||
|
|
@ -402,7 +349,8 @@ export class ImmutableStore<T> extends Store<T> {
|
|||
this.data = data
|
||||
}
|
||||
|
||||
private static readonly pass: () => void = () => {}
|
||||
private static readonly pass: () => void = () => {
|
||||
}
|
||||
|
||||
addCallback(_: (data: T) => void): () => void {
|
||||
// pass: data will never change
|
||||
|
|
@ -430,8 +378,8 @@ export class ImmutableStore<T> extends Store<T> {
|
|||
|
||||
map<J>(
|
||||
f: (t: T) => J,
|
||||
extraStores: Store<any>[] = undefined,
|
||||
ondestroyCallback?: (f: () => void) => void
|
||||
extraStores: Store<any>[] | ((f: () => void) => void)= undefined,
|
||||
ondestroyCallback?: (f: () => void) => void,
|
||||
): ImmutableStore<J> {
|
||||
if (extraStores?.length > 0) {
|
||||
return new MappedStore(this, f, extraStores, undefined, f(this.data), ondestroyCallback)
|
||||
|
|
@ -482,7 +430,6 @@ class ListenerTracker<T> {
|
|||
public ping(data: T): number {
|
||||
this.pingCount++
|
||||
let toDelete = undefined
|
||||
const startTime = new Date().getTime() / 1000
|
||||
for (const callback of this._callbacks) {
|
||||
try {
|
||||
if (callback(data) === true) {
|
||||
|
|
@ -498,12 +445,6 @@ class ListenerTracker<T> {
|
|||
console.error("Got an error while running a callback:", e)
|
||||
}
|
||||
}
|
||||
const endTime = new Date().getTime() / 1000
|
||||
if (endTime - startTime > 500) {
|
||||
console.trace(
|
||||
"Warning: a ping took more then 500ms; this is probably a performance issue"
|
||||
)
|
||||
}
|
||||
if (toDelete !== undefined) {
|
||||
for (const toDeleteElement of toDelete) {
|
||||
this._callbacks.splice(this._callbacks.indexOf(toDeleteElement), 1)
|
||||
|
|
@ -529,6 +470,11 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
private static readonly pass: () => void
|
||||
private readonly _upstream: Store<TIn>
|
||||
private readonly _upstreamCallbackHandler: ListenerTracker<TIn> | undefined
|
||||
/**
|
||||
* THe last 'pingcount' (aka 'tick') that the upstream did.
|
||||
* If we get a request for the data, we check if it is still up to date with this
|
||||
* @private
|
||||
*/
|
||||
private _upstreamPingCount: number = -1
|
||||
private _unregisterFromUpstream: () => void
|
||||
private readonly _f: (t: TIn) => T
|
||||
|
|
@ -540,10 +486,10 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
constructor(
|
||||
upstream: Store<TIn>,
|
||||
f: (t: TIn) => T,
|
||||
extraStores: Store<unknown>[],
|
||||
extraStores: Store<unknown>[] | ((t: () => void) => void),
|
||||
upstreamListenerHandler: ListenerTracker<TIn> | undefined,
|
||||
initialState: T,
|
||||
onDestroy?: (f: () => void) => void
|
||||
onDestroy?: (f: () => void) => void,
|
||||
) {
|
||||
super()
|
||||
this._upstream = upstream
|
||||
|
|
@ -551,7 +497,11 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
this._f = f
|
||||
this._data = initialState
|
||||
this._upstreamPingCount = upstreamListenerHandler?.pingCount
|
||||
this._extraStores = extraStores
|
||||
if (typeof extraStores === "function") {
|
||||
onDestroy ??= extraStores
|
||||
} else {
|
||||
this._extraStores = extraStores
|
||||
}
|
||||
this.registerCallbacksToUpstream()
|
||||
if (onDestroy !== undefined) {
|
||||
onDestroy(() => this.unregisterFromUpstream())
|
||||
|
|
@ -572,7 +522,7 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
if (!this._callbacksAreRegistered) {
|
||||
// Callbacks are not registered, so we haven't been listening for updates from the upstream which might have changed
|
||||
if (this._upstreamCallbackHandler?.pingCount != this._upstreamPingCount) {
|
||||
// Upstream has pinged - let's update our data first
|
||||
// Upstream has pinged at least once - let's update our data first
|
||||
this._data = this._f(this._upstream.data)
|
||||
}
|
||||
return this._data
|
||||
|
|
@ -582,30 +532,34 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
|
||||
map<J>(
|
||||
f: (t: T) => J,
|
||||
extraStores: Store<unknown>[] = undefined,
|
||||
ondestroyCallback?: (f: () => void) => void
|
||||
extraStores: Store<unknown>[] | ((f: () => void) => void) = undefined,
|
||||
ondestroyCallback?: (f: () => void) => void,
|
||||
): Store<J> {
|
||||
let stores: Store<unknown>[] = undefined
|
||||
if (extraStores?.length > 0 || this._extraStores?.length > 0) {
|
||||
stores = []
|
||||
}
|
||||
if (extraStores?.length > 0) {
|
||||
stores?.push(...extraStores)
|
||||
}
|
||||
if (this._extraStores?.length > 0) {
|
||||
this._extraStores?.forEach((store) => {
|
||||
if (stores.indexOf(store) < 0) {
|
||||
stores.push(store)
|
||||
}
|
||||
})
|
||||
if (typeof extraStores !== "function") {
|
||||
|
||||
if (extraStores?.length > 0 || this._extraStores?.length > 0) {
|
||||
stores = []
|
||||
}
|
||||
if (extraStores?.length > 0) {
|
||||
stores?.push(...extraStores)
|
||||
}
|
||||
if (this._extraStores?.length > 0) {
|
||||
this._extraStores?.forEach((store) => {
|
||||
if (stores.indexOf(store) < 0) {
|
||||
stores.push(store)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return new MappedStore(
|
||||
this,
|
||||
f, // we could fuse the functions here (e.g. data => f(this._f(data), but this might result in _f being calculated multiple times, breaking things
|
||||
stores,
|
||||
this._callbacks,
|
||||
f(this.data),
|
||||
ondestroyCallback
|
||||
ondestroyCallback,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -651,7 +605,12 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
}
|
||||
|
||||
private unregisterFromUpstream() {
|
||||
if (!this._callbacksAreRegistered) {
|
||||
return
|
||||
}
|
||||
this._upstreamPingCount = this._upstreamCallbackHandler.pingCount
|
||||
this._callbacksAreRegistered = false
|
||||
console.log("Unregistering from upstream", this._upstream)
|
||||
this._unregisterFromUpstream()
|
||||
this._unregisterFromExtraStores?.forEach((unr) => unr())
|
||||
}
|
||||
|
|
@ -659,7 +618,7 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
private registerCallbacksToUpstream() {
|
||||
this._unregisterFromUpstream = this._upstream.addCallback(() => this.update())
|
||||
this._unregisterFromExtraStores = this._extraStores?.map((store) =>
|
||||
store?.addCallback(() => this.update())
|
||||
store?.addCallback(() => this.update()),
|
||||
)
|
||||
this._callbacksAreRegistered = true
|
||||
}
|
||||
|
|
@ -680,7 +639,8 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
}
|
||||
|
||||
export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||
private static readonly pass: () => void = () => {}
|
||||
private static readonly pass: () => void = () => {
|
||||
}
|
||||
public data: T
|
||||
_callbacks: ListenerTracker<T> = new ListenerTracker<T>()
|
||||
|
||||
|
|
@ -695,7 +655,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
|
||||
public static flatten<X>(
|
||||
source: Store<Store<X>>,
|
||||
possibleSources?: Store<object>[]
|
||||
possibleSources?: Store<object>[],
|
||||
): UIEventSource<X> {
|
||||
const sink = new UIEventSource<X>(source.data?.data)
|
||||
|
||||
|
|
@ -722,9 +682,9 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
* Converts a promise into a UIventsource, sets the UIeventSource when the result is calculated.
|
||||
* If the promise fails, the value will stay undefined, but 'onError' will be called
|
||||
*/
|
||||
public static FromPromise<T>(
|
||||
public static fromPromise<T>(
|
||||
promise: Promise<T>,
|
||||
onError: (e) => void = undefined
|
||||
onError: (e) => void = undefined,
|
||||
): UIEventSource<T> {
|
||||
const src = new UIEventSource<T>(undefined)
|
||||
promise?.then((d) => src.setData(d))
|
||||
|
|
@ -744,8 +704,8 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
* @param promise
|
||||
* @constructor
|
||||
*/
|
||||
public static FromPromiseWithErr<T>(
|
||||
promise: Promise<T>
|
||||
public static fromPromiseWithErr<T>(
|
||||
promise: Promise<T>,
|
||||
): UIEventSource<{ success: T } | { error: any } | undefined> {
|
||||
const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
|
||||
promise
|
||||
|
|
@ -778,7 +738,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
return undefined
|
||||
}
|
||||
return "" + fl
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -809,7 +769,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
return undefined
|
||||
}
|
||||
return "" + fl
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -817,13 +777,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
return stringUIEventSource.sync(
|
||||
(str) => str === "true",
|
||||
[],
|
||||
(b) => "" + b
|
||||
(b) => "" + b,
|
||||
)
|
||||
}
|
||||
|
||||
static asObject<T extends object | string>(
|
||||
stringUIEventSource: UIEventSource<string>,
|
||||
defaultV: T
|
||||
defaultV: T,
|
||||
): UIEventSource<T> {
|
||||
return stringUIEventSource.sync(
|
||||
(str) => {
|
||||
|
|
@ -839,13 +799,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
"due to",
|
||||
e,
|
||||
"; the underlying data store has tag",
|
||||
stringUIEventSource.tag
|
||||
stringUIEventSource.tag,
|
||||
)
|
||||
return defaultV
|
||||
}
|
||||
},
|
||||
[],
|
||||
(b) => JSON.stringify(b) ?? ""
|
||||
(b) => JSON.stringify(b) ?? "",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -934,8 +894,17 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
*/
|
||||
public map<J>(
|
||||
f: (t: T) => J,
|
||||
extraSources: Store<unknown>[] = [],
|
||||
onDestroy?: (f: () => void) => void
|
||||
extraSources?: Store<unknown>[],
|
||||
onDestroy?: (f: () => void) => void,
|
||||
)
|
||||
public map<J>(
|
||||
f: (t: T) => J,
|
||||
onDestroy: (f: () => void) => void,
|
||||
)
|
||||
public map<J>(
|
||||
f: (t: T) => J,
|
||||
extraSources: Store<unknown>[] | ((f: () => void) => void),
|
||||
onDestroy?: (f: () => void) => void,
|
||||
): Store<J> {
|
||||
return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy)
|
||||
}
|
||||
|
|
@ -946,8 +915,8 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
*/
|
||||
public mapD<J>(
|
||||
f: (t: Exclude<T, undefined | null>) => J,
|
||||
extraSources: Store<unknown>[] = [],
|
||||
callbackDestroyFunction?: (f: () => void) => void
|
||||
extraSources?: Store<unknown>[] | ((f: () => void) => void),
|
||||
callbackDestroyFunction?: (f: () => void) => void,
|
||||
): Store<J | undefined> {
|
||||
return new MappedStore(
|
||||
this,
|
||||
|
|
@ -965,12 +934,12 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
this.data === undefined || this.data === null
|
||||
? <undefined | null>this.data
|
||||
: f(<Exclude<T, undefined | null>>this.data),
|
||||
callbackDestroyFunction
|
||||
callbackDestroyFunction,
|
||||
)
|
||||
}
|
||||
|
||||
public mapAsyncD<J>(f: (t: T) => Promise<J>): Store<J> {
|
||||
return this.bindD((t) => UIEventSource.FromPromise(f(t)))
|
||||
return this.bindD((t) => UIEventSource.fromPromise(f(t)))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -985,7 +954,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
f: (t: T) => J,
|
||||
extraSources: Store<unknown>[],
|
||||
g: (j: J, t: T) => T,
|
||||
allowUnregister = false
|
||||
allowUnregister = false,
|
||||
): UIEventSource<J> {
|
||||
const stack = new Error().stack.split("\n")
|
||||
const callee = stack[1]
|
||||
|
|
@ -1033,4 +1002,16 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
update(f: Updater<T> & ((value: T) => T)): void {
|
||||
this.setData(f(this.data))
|
||||
}
|
||||
|
||||
public static concat<T>(stores: ReadonlyArray<UIEventSource<T | undefined>>): UIEventSource<(T | undefined)[]> ;
|
||||
public static concat<T>(stores: ReadonlyArray<UIEventSource<T>>): UIEventSource<T[]> ;
|
||||
public static concat<T>(stores: ReadonlyArray<UIEventSource<T | undefined>>): UIEventSource<(T | undefined)[]> {
|
||||
const newStore = <UIEventSource<T[]>>Stores.concat(stores)
|
||||
newStore.addCallbackD(list => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
stores[i]?.setData(list[i])
|
||||
}
|
||||
})
|
||||
return newStore
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class AndroidPolyfill {
|
|||
* @private
|
||||
*/
|
||||
private static backfillGeolocation(databridgePlugin: DatabridgePlugin) {
|
||||
const src = UIEventSource.FromPromise(
|
||||
const src = UIEventSource.fromPromise(
|
||||
databridgePlugin.request({ key: "location:has-permission" })
|
||||
)
|
||||
src.addCallbackAndRunD((permission) => {
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export default class Wikidata {
|
|||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key))
|
||||
const src = UIEventSource.fromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key))
|
||||
Wikidata._storeCache.set(key, src)
|
||||
return src
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue