Skip to content
Snippets Groups Projects
snapshots.ts 7.39 KiB
Newer Older
David Sehnal's avatar
David Sehnal committed
/**
 * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
 *
 * @author David Sehnal <david.sehnal@gmail.com>
 */

import { List } from 'immutable';
David Sehnal's avatar
David Sehnal committed
import { UUID } from 'mol-util';
import { PluginState } from '../state';
import { PluginComponent } from 'mol-plugin/component';
import { PluginContext } from 'mol-plugin/context';
David Sehnal's avatar
David Sehnal committed

export { PluginStateSnapshotManager }

class PluginStateSnapshotManager extends PluginComponent<{
    current?: UUID | undefined,
    entries: List<PluginStateSnapshotManager.Entry>,
    isPlaying: boolean,
    nextSnapshotDelayInMs: number
    static DefaultNextSnapshotDelayInMs = 1500;

    private entryMap = new Map<string, PluginStateSnapshotManager.Entry>();

David Sehnal's avatar
David Sehnal committed
    readonly events = {
        changed: this.ev()
    };

    currentGetSnapshotParams: PluginState.GetSnapshotParams = PluginState.DefaultGetSnapshotParams as any;

    getIndex(e: PluginStateSnapshotManager.Entry) {
        return this.state.entries.indexOf(e);
    }

    getEntry(id: string | undefined) {
        if (!id) return;
        return this.entryMap.get(id);
David Sehnal's avatar
David Sehnal committed
    }

    remove(id: string) {
        const e = this.entryMap.get(id);
        this.entryMap.delete(id);
David Sehnal's avatar
David Sehnal committed
        this.updateState({
            current: this.state.current === id ? void 0 : this.state.current,
            entries: this.state.entries.delete(this.getIndex(e))
David Sehnal's avatar
David Sehnal committed
        });
David Sehnal's avatar
David Sehnal committed
        this.events.changed.next();
    }

    add(e: PluginStateSnapshotManager.Entry) {
        this.entryMap.set(e.snapshot.id, e);
        this.updateState({ current: e.snapshot.id, entries: this.state.entries.push(e) });
        this.events.changed.next();
    }

    replace(id: string, snapshot: PluginState.Snapshot) {
        const old = this.getEntry(id);
        if (!old) return;

        const idx = this.getIndex(old);
        // The id changes here!
        const e = PluginStateSnapshotManager.Entry(snapshot, {
            name: old.name,
            description: old.description
        });
        this.entryMap.set(snapshot.id, e);
        this.updateState({ current: e.snapshot.id, entries: this.state.entries.set(idx, e) });
        this.events.changed.next();
    }

    move(id: string, dir: -1 | 1) {
        const len = this.state.entries.size;
        if (len < 2) return;

        const e = this.getEntry(id);
        if (!e) return;
        const from = this.getIndex(e);
        let to = (from + dir) % len;
        if (to < 0) to += len;
        const f = this.state.entries.get(to);

        const entries = this.state.entries.asMutable();
        entries.set(to, e);
        entries.set(from, f);

        this.updateState({ current: e.snapshot.id, entries: entries.asImmutable() });
David Sehnal's avatar
David Sehnal committed
        this.events.changed.next();
    }

    clear() {
        if (this.state.entries.size === 0) return;
        this.entryMap.clear();
        this.updateState({ current: void 0, entries: List<PluginStateSnapshotManager.Entry>() });
David Sehnal's avatar
David Sehnal committed
        this.events.changed.next();
    }

David Sehnal's avatar
David Sehnal committed
    setCurrent(id: string) {
        const e = this.getEntry(id);
        if (e) {
            this.updateState({ current: id as UUID });
            this.events.changed.next();
        }
        return e && e.snapshot;
    }

    getNextId(id: string | undefined, dir: -1 | 1) {
        const len = this.state.entries.size;
        if (!id) {
            if (len === 0) return void 0;
            const idx = dir === -1 ? len - 1 : 0;
            return this.state.entries.get(idx).snapshot.id;

        const e = this.getEntry(id);
        if (!e) return;
        let idx = this.getIndex(e);
        if (idx < 0) return;

        idx = (idx + dir) % len;
        if (idx < 0) idx += len;

        return this.state.entries.get(idx).snapshot.id;
    async setRemoteSnapshot(snapshot: PluginStateSnapshotManager.RemoteSnapshot): Promise<PluginState.Snapshot | undefined> {
David Sehnal's avatar
David Sehnal committed
        this.clear();
        const entries = List<PluginStateSnapshotManager.Entry>().asMutable()
        for (const e of snapshot.entries) {
            this.entryMap.set(e.snapshot.id, e);
David Sehnal's avatar
David Sehnal committed
        const current = snapshot.current
            ? snapshot.current
            : snapshot.entries.length > 0
            ? snapshot.entries[0].snapshot.id
            : void 0;
        this.updateState({
            current,
            entries: entries.asImmutable(),
            isPlaying: false,
            nextSnapshotDelayInMs: snapshot.playback ? snapshot.playback.nextSnapshotDelayInMs : PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
        });
David Sehnal's avatar
David Sehnal committed
        this.events.changed.next();
        if (!current) return;
        const entry = this.getEntry(current);
        const next = entry && entry.snapshot;
        if (!next) return;
        await this.plugin.state.setSnapshot(next);
        if (snapshot.playback &&  snapshot.playback.isPlaying) this.play();
        return next;
David Sehnal's avatar
David Sehnal committed
    }

    getRemoteSnapshot(options?: { name?: string, description?: string }): PluginStateSnapshotManager.RemoteSnapshot {
        // TODO: diffing and all that fancy stuff
        return {
            timestamp: +new Date(),
            name: options && options.name,
            description: options && options.description,
            current: this.state.current,
            playback: {
                isPlaying: this.state.isPlaying,
                nextSnapshotDelayInMs: this.state.nextSnapshotDelayInMs
            },
David Sehnal's avatar
David Sehnal committed
            entries: this.state.entries.valueSeq().toArray()
        };
    }

    private timeoutHandle: any = void 0;
    private next = async () => {
        this.timeoutHandle = void 0;
        const next = this.getNextId(this.state.current, 1);
        if (!next || next === this.state.current) {
            this.stop();
            return;
        }
        const snapshot = this.setCurrent(next)!;
        await this.plugin.state.setSnapshot(snapshot);
        const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
        if (this.state.isPlaying) this.timeoutHandle = setTimeout(this.next, delay);
    };

    play() {
        this.updateState({ isPlaying: true });
        this.next();
    }

    stop() {
        this.updateState({ isPlaying: false });
        if (typeof this.timeoutHandle !== 'undefined') clearTimeout(this.timeoutHandle);
        this.timeoutHandle = void 0;
        this.events.changed.next();
    }

    togglePlay() {
        if (this.state.isPlaying) this.stop();
        else this.play();
    }

    constructor(private plugin: PluginContext) {
        super({
            current: void 0,
            entries: List(),
            isPlaying: false,
            nextSnapshotDelayInMs: PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
        });
        // TODO make nextSnapshotDelayInMs editable
David Sehnal's avatar
David Sehnal committed
    }
}

namespace PluginStateSnapshotManager {
    export interface Entry {
David Sehnal's avatar
David Sehnal committed
        timestamp: number,
David Sehnal's avatar
David Sehnal committed
        name?: string,
David Sehnal's avatar
David Sehnal committed
        description?: string,
        snapshot: PluginState.Snapshot
    }

    export function Entry(snapshot: PluginState.Snapshot, params: {name?: string, description?: string }): Entry {
        return { timestamp: +new Date(), snapshot, ...params };
David Sehnal's avatar
David Sehnal committed
    export interface RemoteSnapshot {
        timestamp: number,
        name?: string,
        description?: string,
        current: UUID | undefined,
        playback: {
            isPlaying: boolean,
            nextSnapshotDelayInMs: number,
        },
David Sehnal's avatar
David Sehnal committed
        entries: Entry[]
    }
}