opendash
Version:
open.DASH data visualization framework
288 lines (222 loc) • 7.69 kB
text/typescript
import produce from "immer";
import {
AppInterface,
ServicesInterface,
BaseService,
DataAdapterInterface,
DataAdapterContext,
DataItemInterface,
DataItemValueInterface,
equals,
DataItemHistoryOptionsInterface,
DataItemIdentifierInterface,
DataItemDimensionHistoryOptionsInterface,
DataItemDimensionValueInterface,
} from "..";
type SubscriptionCallback = () => void;
export class DataService extends BaseService {
private app: AppInterface;
private services: ServicesInterface;
private adapter: DataAdapterInterface;
private context: DataAdapterContext;
private itemStore = new Map<string, DataItemInterface>();
private itemSubscriptions = new Map<string, Set<SubscriptionCallback>>();
private itemListSubscriptions: Set<SubscriptionCallback> = new Set();
private valueStore = new Map<string, DataItemValueInterface>();
private valueSubscriptions = new Map<string, Set<SubscriptionCallback>>();
constructor(app: AppInterface, adapter: DataAdapterInterface) {
super();
this.app = app;
this.services = app.services;
this.adapter = adapter;
this.context = new DataAdapterContext(this, this.app);
this.initAdapter(adapter, this.context, this.services);
}
private notifyDataSubscribers(
subscribers: Set<SubscriptionCallback>,
clear: boolean = false
): void {
subscribers.forEach((callback) => {
try {
callback();
} catch (error) {
console.error("Error in subscription:", error);
}
});
if (clear) {
subscribers.clear();
}
}
public subscribeItemList(callback: SubscriptionCallback) {
this.itemListSubscriptions.add(callback);
return () => {
this.itemListSubscriptions.delete(callback);
};
}
public subscribeItem(
item: DataItemInterface,
callback: SubscriptionCallback
) {
const key = keyForItem(item);
if (!this.itemSubscriptions.has(key)) {
this.itemSubscriptions.set(key, new Set());
}
this.itemSubscriptions.get(key).add(callback);
return () => {
this.itemSubscriptions.get(key).delete(callback);
};
}
public subscribeValue(
item: DataItemInterface,
callback: SubscriptionCallback
) {
const key = keyForItem(item);
if (!this.valueSubscriptions.has(key)) {
this.valueSubscriptions.set(key, new Set());
}
this.valueSubscriptions.get(key).add(callback);
return () => {
this.valueSubscriptions.get(key).delete(callback);
};
}
public async get(source: string, id: string): Promise<DataItemInterface> {
return this._getOrThrowSync(source, id);
}
public _getOrThrowSync(source: string, id: string): DataItemInterface {
const item = this.itemStore.get(keyForIdentifier([source, id]));
if (!item) {
throw new Error(`Item with id "${id}" not found`);
}
return item;
}
public async getValue(
item: DataItemInterface
): Promise<DataItemValueInterface> {
return this._getValueOrThrowSync(item);
}
public _getValueOrThrowSync(item: DataItemInterface): DataItemValueInterface {
return this.valueStore.get(keyForItem(item));
}
public async list(): Promise<DataItemInterface[]> {
return this._listOrThrowSync();
}
public _listOrThrowSync(): DataItemInterface[] {
return Array.from(this.itemStore.values());
}
public async setItem(item: DataItemInterface): Promise<void> {
const key = keyForItem(item);
if (!this.itemSubscriptions.has(key)) {
this.itemSubscriptions.set(key, new Set());
}
if (!this.itemStore.has(key) || equals(this.itemStore.get(key), item)) {
this.itemStore.set(key, item);
this.notifyDataSubscribers(this.itemListSubscriptions);
this.notifyDataSubscribers(this.itemSubscriptions.get(key));
}
}
public async setItems(items: DataItemInterface[]): Promise<void> {
for (const item of items) {
const key = keyForItem(item);
if (!this.itemSubscriptions.has(key)) {
this.itemSubscriptions.set(key, new Set());
}
if (!this.itemStore.has(key) || !equals(this.itemStore.get(key), item)) {
this.itemStore.set(key, item);
this.notifyDataSubscribers(this.itemSubscriptions.get(key));
}
}
this.notifyDataSubscribers(this.itemListSubscriptions);
}
public async setValue(
item: DataItemInterface,
value: DataItemValueInterface
): Promise<void> {
const key = keyForItem(item);
if (!this.valueSubscriptions.has(key)) {
this.valueSubscriptions.set(key, new Set());
}
if (!this.valueStore.has(key) || !equals(this.valueStore.get(key), value)) {
this.valueStore.set(key, value);
this.notifyDataSubscribers(this.valueSubscriptions.get(key));
}
}
public async removeItem(item: DataItemInterface): Promise<void> {
const key = keyForItem(item);
this.itemStore.delete(key);
this.valueStore.delete(key);
this.notifyDataSubscribers(this.itemListSubscriptions);
this.notifyDataSubscribers(this.itemSubscriptions.get(key));
this.notifyDataSubscribers(this.valueSubscriptions.get(key));
}
public async clear() {
this.setLoading(true);
// clear stores
this.itemStore.clear();
this.valueStore.clear();
this.notifyDataSubscribers(this.itemListSubscriptions);
Array.from(this.itemSubscriptions.keys()).forEach((key) => {
this.notifyDataSubscribers(this.itemSubscriptions.get(key));
});
Array.from(this.valueSubscriptions.keys()).forEach((key) => {
this.notifyDataSubscribers(this.valueSubscriptions.get(key));
});
}
public async fetchValues(
item: DataItemInterface,
options: DataItemHistoryOptionsInterface
): Promise<DataItemValueInterface[]> {
await this.wait();
return await this.adapter.fetchValues(item, options);
}
public async fetchValuesMultiItem(
items: DataItemInterface[],
options: DataItemHistoryOptionsInterface
): Promise<[DataItemInterface, DataItemValueInterface[]][]> {
await this.wait();
const historyRequests = items.map((item) =>
this.adapter.fetchValues(item, options)
);
const histories = await Promise.all(historyRequests);
const resultEntries = items.map(
(item, i) =>
[item, histories[i]] as [DataItemInterface, DataItemValueInterface[]]
);
return resultEntries;
}
public async fetchDimensionValues(
item: DataItemInterface,
dimension: number,
options: DataItemDimensionHistoryOptionsInterface
): Promise<DataItemDimensionValueInterface[]> {
await this.wait();
return await this.adapter.fetchDimensionValues(item, dimension, options);
}
public async fetchDimensionValuesMultiItem(
items: [DataItemInterface, number][],
options: DataItemHistoryOptionsInterface
): Promise<[DataItemInterface, number, DataItemDimensionValueInterface[]][]> {
await this.wait();
const historyRequests = items.map(([item, dimension]) =>
this.adapter.fetchDimensionValues(item, dimension, options)
);
const histories = await Promise.all(historyRequests);
const resultEntries = items.map(
([item, dimension], i) =>
[item, dimension, histories[i]] as [
DataItemInterface,
number,
DataItemDimensionValueInterface[]
]
);
return resultEntries;
}
public async update(item: DataItemInterface): Promise<void> {
return await this.adapter.update(item);
}
}
function keyForIdentifier(item: DataItemIdentifierInterface): string {
return JSON.stringify(item);
}
function keyForItem(item: DataItemInterface): string {
return JSON.stringify([item.source, item.id]);
}