scrivito
Version:
Scrivito is a professional, yet easy to use SaaS Enterprise Content Management Service, built for digital agencies and medium to large businesses. It is completely maintenance-free, cost-effective, and has unprecedented performance and security.
138 lines (108 loc) • 3.51 kB
text/typescript
import { InternalError } from 'scrivito_sdk/common';
import {
LoadableMeta,
LoadableState,
LoadableVersion,
} from 'scrivito_sdk/loadable/loadable_state';
import { LoaderProcess } from 'scrivito_sdk/loadable/loading_registry';
import { StateContainer, addBatchUpdate } from 'scrivito_sdk/state';
export type LoaderCallback<T> = () => Promise<T>;
export type InvalidationCallback = () => LoadableVersion;
type LoadId = number;
let loadIdCounter = 0;
export class LoaderCallbackProcess<LoadableType> implements LoaderProcess {
private currentLoad: LoadId | undefined;
constructor(
private readonly stateContainer: StateContainer<
LoadableState<LoadableType>
>,
private readonly loader: LoaderCallback<LoadableType>,
private readonly invalidation?: InvalidationCallback,
private readonly onChange?: () => void
) {}
notifyDataRequired() {
this.triggerLoadingIfNeeded();
}
notifyDataNoLongerRequired() {
// don't care
}
notifyDataWasSet() {
// when data was set, discard any loading that may still be ongoing
this.currentLoad = undefined;
const onChange = this.onChange;
if (onChange) {
onChange();
}
}
setTidyCallback() {
// this process currently never tidies itself up.
// data loaded via this process is intended to be "cached forever" anyway.
}
// trigger loading the data.
// does nothing if the data is already loading, or no loading is needed.
private async triggerLoadingIfNeeded() {
if (this.isLoading()) return;
const versionWhenLoadingStarted = versionFromCallback(this.invalidation);
if (!this.loadingNeeded(versionWhenLoadingStarted)) return;
const loadId = loadIdCounter++;
const finishLoader = (effect: () => void) => {
if (this.currentLoad === loadId) {
addBatchUpdate(() => {
effect();
this.currentLoad = undefined;
if (this.onChange) {
this.onChange();
}
});
}
};
this.currentLoad = loadId;
try {
const result = await this.loader();
finishLoader(() =>
this.stateContainer.set({
value: result,
meta: { version: versionWhenLoadingStarted },
})
);
} catch (error) {
finishLoader(() =>
this.stateContainer.set({
meta: { error, version: versionWhenLoadingStarted },
})
);
}
}
private loadingNeeded(currentVersion?: LoadableVersion): boolean {
const metaStateContainer = this.stateContainer.subState('meta');
const meta = metaStateContainer.get();
// not yet loaded?
if (meta === undefined) return true;
// no invalidation used (and therefore up-to-date by definition)?
if (currentVersion === undefined) return false;
// not up-to-date?
return currentVersion !== meta.version;
}
private isLoading() {
return this.currentLoad !== undefined;
}
}
export function metaHasBeenInvalidated(
meta: LoadableMeta | undefined,
callback?: InvalidationCallback
) {
if (!callback || meta === undefined) return false;
return versionFromCallback(callback) !== meta.version;
}
export function versionFromCallback(callback?: InvalidationCallback) {
if (!callback) {
return;
}
const version = callback();
// protect against "crazy" objects like NaN
if (typeof version === 'number' && isNaN(version)) {
// invalidation callback returned unsuitable version
throw new InternalError();
}
return version;
}