UNPKG

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.

229 lines (183 loc) 6.53 kB
import { InternalError, isPresent } from 'scrivito_sdk/common'; import { conservativeUpdate } from 'scrivito_sdk/state/conservative_update'; import { failIfFrozen } from 'scrivito_sdk/state/frozen'; import { notifySubscribers } from 'scrivito_sdk/state/subscribers'; import { recordDetector } from 'scrivito_sdk/state/track_state_access'; /** Get the key type for a subState. * * Since `undefined` and `null` are automatically handled by subState, * they can be ignored for the key type. * (otherwise TS infers: `keyof null` is `never`) */ type SubKey<StateType> = keyof NonNullable<StateType> & string; /** Get the type of a subState. * * When using subState, `undefined` and `null` are automatically handled: * -> a parent state of null or undefined automatically leads to the value undefined for the child. * -> get and set always use 'T | undefined', therefore: * StateContainer<T> is equivalent to StateContainer<T | undefined> * and StateContainer<NotUndefined<T>> */ type SubType<StateType, Key extends SubKey<StateType>> = NotUndefined< NonNullable<StateType>[Key] >; type NotUndefined<T> = Exclude<T, undefined>; export interface StateReader<StateType> { id(): string; get(): StateType | undefined; subState<Key extends SubKey<StateType>>( key: Key ): StateReader<SubType<StateType, Key>>; } export interface StateContainer<StateType> extends StateReader<StateType> { set(newState: StateType | undefined): void; clear(): void; reader(): StateReader<StateType>; subState<Key extends SubKey<StateType>>( key: Key ): StateContainer<SubType<StateType, Key>>; } // abstract interface for managing state abstract class AbstractStateStore<StateType> implements StateContainer<StateType> { // return current state get() { const valueWhenAccessed = this.untrackedGet(); recordDetector(() => valueWhenAccessed !== this.untrackedGet()); return valueWhenAccessed; } abstract untrackedGet(): StateType | undefined; set(newState: StateType | undefined): void { const currentState = this.untrackedGet(); const updatedState = conservativeUpdate(currentState, newState); if (updatedState === currentState) { return; } this.uncheckedSet(updatedState); } abstract uncheckedSet(newState: StateType | undefined): void; // get a string that uniquely identifies this state abstract id(): string; // reset the state back to undefined clear() { this.set(undefined); } // this method may only be called when StateType is fully partial, // i.e. all properties defined by StateType are optional. subState<Key extends SubKey<StateType>>( key: Key ): StateContainer<SubType<StateType, Key>> { return new StateTreeNode(this, key); } reader(): StateReader<StateType> { // identical implementation, different type return this; } // this method may only be called when StateType is fully partial, // i.e. all properties defined by StateType are optional (= may be undefined). setSubState<K extends SubKey<StateType>>( key: K, newSubState: SubType<StateType, K> | undefined ) { const priorState = this.untrackedGet(); if (priorState === undefined) { const newState = { [key]: newSubState }; // Since StateType is fully partial, newState is a valid StateType. // No way to tell TypeScript this, though. this.uncheckedSet(newState as unknown as StateType); return; } if (priorState === null) { // if StateType includes null, then it is not fully partial // and this methods should not be used! throw new InternalError(); } if (newSubState === undefined) { const priorKeys = Object.keys(priorState); if (priorKeys.length === 1 && priorKeys[0] === key) { // remove empty objects, avoid memory leak this.uncheckedSet(undefined); return; } } performAsStateChange(() => { if (newSubState === undefined) { // remove undefined keys, avoid memory leak delete priorState[key]; } else { // Since StateType is fully partial, this is true: // (SubType<StateType, K> | undefined) == SubType<StateType, K> priorState[key] = newSubState as SubType<StateType, K>; } }); } getSubState<K extends SubKey<StateType>>( key: K ): SubType<StateType, K> | undefined { const state = this.untrackedGet(); if (isPresent(state)) { // we know that state is neither null or undefined const nonNullState = state as NonNullable<typeof state>; const subState = nonNullState[key]; // if T includes undefined, it is equal to (NotUndefined<T> | undefined) // if T does not include undefined, it is equal to NotUndefined<T> return subState as NotUndefined<typeof subState> | undefined; } } } // a state tree, which can be used to store state. // this is the root of the tree, which keeps the state of the entire tree. export class StateTree<TreeType> extends AbstractStateStore<TreeType> { private state?: TreeType; constructor() { super(); } untrackedGet() { return this.state; } uncheckedSet(newState: TreeType) { performAsStateChange(() => { this.state = newState; }); } id() { return ''; } } function performAsStateChange(actualChange: () => void): void { failIfFrozen('Changing state'); actualChange(); notifySubscribers(); } // a node of a state tree. // does not actually keep state, but provides // access scoped to a subtree of a StateTree. class StateTreeNode< ParentType, Key extends SubKey<ParentType> > extends AbstractStateStore<SubType<ParentType, Key>> { private parentState: AbstractStateStore<ParentType>; private key: Key; private cachedId?: string; constructor(parentState: AbstractStateStore<ParentType>, key: Key) { super(); this.parentState = parentState; this.key = key; } untrackedGet(): SubType<ParentType, Key> | undefined { return this.parentState.getSubState(this.key); } uncheckedSet(newState: SubType<ParentType, Key> | undefined) { this.parentState.setSubState(this.key, newState); } id(): string { if (this.cachedId === undefined) { // first convert backslash to double-backslash // then convert slash to backslash-slash const escapedKey = this.key.replace(/\\/g, '\\\\').replace(/\//g, '\\/'); this.cachedId = `${this.parentState.id()}/${escapedKey}`; } return this.cachedId; } }