aboutsummaryrefslogtreecommitdiff
path: root/src/fields/Proxy.ts
blob: 21b9fe89b6800b555144f4a5ac47c5e214912b2c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { action, computed, observable, runInAction } from 'mobx';
import { primitive, serializable } from 'serializr';
import { DocServer } from '../client/DocServer';
import { scriptingGlobal } from '../client/util/ScriptingGlobals';
import { Deserializable } from '../client/util/SerializationHelper';
import { Doc, Field, FieldWaiting, Opt } from './Doc';
import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols';
import { ObjectField } from './ObjectField';

type serializedProxyType = { cache: { field: unknown; p: undefined | Promise<unknown> }; fieldId: string };

function deserializeProxy(field: serializedProxyType) {
    if (!field.cache.field) {
        field.cache = { field: DocServer.GetCachedRefField(field.fieldId), p: undefined };
    }
}
@Deserializable('proxy', (obj: unknown) => deserializeProxy(obj as serializedProxyType))
export class ProxyField<T extends Doc> extends ObjectField {
    @serializable(primitive())
    readonly fieldId: string = '';

    private failed = false;

    // This getter/setter and nested object thing is
    // because mobx doesn't play well with observable proxies
    @observable.ref
    private _cache: { readonly field: T | undefined; p: FieldWaiting<T> | undefined } = { field: undefined, p: undefined };
    private get cache(): { field: T | undefined; p: FieldWaiting<T> | undefined } {
        return this._cache;
    }
    private set cache(val: { field: T | undefined; p: FieldWaiting<T> | undefined }) {
        runInAction(() => (this._cache = { ...val }));
    }

    constructor();
    constructor(value: T);
    constructor(fieldId: string);
    constructor(value: T | string);
    constructor(value?: T | string) {
        super();
        if (typeof value === 'string') {
            // this.cache = DocServer.GetCachedRefField(value) as any;
            this.fieldId = value;
        } else if (value) {
            this.cache = { field: value, p: undefined };
            this.fieldId = value[Id];
        }
    }

    [ToValue]() {
        return ProxyField.toValue(this);
    }

    [Copy]() {
        return new ProxyField<T>(this.cache.field ?? this.fieldId);
    }

    [ToJavascriptString]() {
        return Field.toScriptString(this[ToValue]()?.value as T);
    }
    [ToScriptString]() {
        return Field.toScriptString(this[ToValue]()?.value as T); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ?
    }
    [ToString]() {
        return Field.toString(this[ToValue]()?.value as T);
    }

    @computed get value(): T | undefined | FieldWaiting<T> {
        if (this.cache.field) return this.cache.field;
        if (this.failed) return undefined;

        this.cache.field = DocServer.GetCachedRefField(this.fieldId) as T;
        if (!this.cache.field && !this.cache.p) {
            this.cache = {
                field: undefined,
                p: DocServer.GetRefField(this.fieldId).then(val => this.setValue(val as T)) as FieldWaiting<T>,
            };
        }
        return this.cache.field ?? this.cache.p;
    }
    @computed get needsRequesting(): boolean {
        return !!(!this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId));
    }

    setExternalValuePromise(externalValuePromise: Promise<unknown>) {
        this.cache.p = externalValuePromise.then(() => this.value) as FieldWaiting<T>;
    }
    @action
    setValue(field: Opt<T>) {
        this.cache = { field, p: undefined };
        this.failed = field === undefined;
        return field;
    }
}
export namespace ProxyField {
    let useProxy = true;
    export function DisableDereference<T>(fn: () => T) {
        useProxy = false;
        try {
            return fn();
        } finally {
            useProxy = true;
        }
    }

    export function toValue(value: { value: unknown }) {
        return useProxy ? { value: value.value } : undefined;
    }
}

// eslint-disable-next-line no-use-before-define
function prefetchValue(proxy: PrefetchProxy<Doc>) {
    return proxy.value as Promise<Doc>;
}

@scriptingGlobal
// eslint-disable-next-line no-use-before-define
@Deserializable('prefetch_proxy', (obj: unknown) => prefetchValue(obj as PrefetchProxy<Doc>))
export class PrefetchProxy<T extends Doc> extends ProxyField<T> {}