{"version":3,"sources":["../../../packages/core/data/query-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,UAAU,EAAsD,MAAM,MAAM,CAAC;AAUpG;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,OAAO;IACtC;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,oBAAY,iBAAiB,CAAC,KAAK,EAAE,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,KAAK,UAAU,CAAC,KAAK,CAAC,CAAC;AAEtF;;GAEG;AACH,oBAAY,yBAAyB,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;AAE5E;;GAEG;AACH,oBAAY,iBAAiB,GAAG,MAAM,IAAI,CAAC;AA6D3C;;;;;;;;;;GAUG;AACH,qBAAa,UAAU,CAAC,KAAK,EAAE,OAAO;IA2E9B,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,eAAe,CAAC;IACxB,OAAO,CAAC,OAAO,CAAC;IA5EpB;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,EAAE,CAAS;IAE1B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS,CAAqB;IAE7C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc,CAAM;IAEnC;;OAEG;IACH,OAAO,CAAC,UAAU,CAAkC;IAEpD;;OAEG;IACH,OAAO,CAAC,OAAO,CAA6B;IAE5C;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAkB;IAElC;;OAEG;IACH,OAAO,CAAC,GAAG,CAAS;IAEpB;;OAEG;IACH,OAAO,CAAC,UAAU,CAAM;IAExB;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAA6B;IAErD;;OAEG;IACH,OAAO,CAAC,eAAe,CAAkB;IAEzC;;OAEG;IACH,OAAO,CAAC,EAAE,CAAqE;IAE/E;;OAEG;IACH,OAAO,CAAC,IAAI,CAAS;IAErB;;OAEG;IACH,OAAO,CAAC,SAAS,CAAS;IAE1B;;;;;;OAMG;gBAES,MAAM,EAAE,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,EACzC,eAAe,CAAC,EAAE,yBAAyB,CAAC,OAAO,CAAC,EACpD,OAAO,CAAC,EAAE,iBAAiB;IAGvC;;;;OAIG;WACW,iBAAiB,IAAI,IAAI;IAIvC;;;;;OAKG;IACI,gBAAgB,CAAC,gBAAgB,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC;IA2BzF;;;OAGG;IACI,UAAU,IAAI,IAAI;IAezB;;;;;;;;OAQG;IACI,KAAK,CAAC,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,IAAI;IAwBvD;;OAEG;IACI,OAAO,IAAI,IAAI;IAgBtB;;;;;;;OAOG;IACI,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI;IAmDxC;;;;OAIG;IACI,KAAK,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI;IAM/B;;;;OAIG;IACI,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMhC,OAAO,CAAC,GAAG;IAKX,OAAO,CAAC,uBAAuB;IA8E/B,OAAO,CAAC,mBAAmB;IA4D3B,OAAO,CAAC,OAAO;IAyCf;;;;OAIG;IACH,OAAO,CAAC,MAAM;IA4Cd,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,cAAc;IAkDtB,OAAO,CAAC,aAAa;CAsBxB","file":"query-cache.d.ts","sourcesContent":["import { EMPTY, merge, Observable, Observer, of, ReplaySubject, Subject, Subscription } from 'rxjs';\r\nimport { delay, expand, multicast, refCount, switchMap } from 'rxjs/operators';\r\nimport { Logging } from '../diagnostics/logging';\r\nimport { Strings } from '../generated/strings';\r\n\r\n// test logging hook.\r\n// function testLog(message: string): void {\r\n//     (<any>window).testLog(message);\r\n// }\r\n\r\n/**\r\n * Query cache options\r\n */\r\nexport interface QueryCacheOptions<TParams> {\r\n    /**\r\n     * parameters passed to create observable.\r\n     */\r\n    params?: TParams;\r\n\r\n    /**\r\n     * interval with milliseconds to make auto background query.\r\n     */\r\n    interval?: number;\r\n\r\n    /**\r\n     * enable recovery capability.\r\n     * caller should clean up environment by calling clearCache() when finished to use the instance of query cache.\r\n     */\r\n    enableRecovery?: boolean;\r\n\r\n    /**\r\n     * enable delaying clean up of observable after all subscriptions were unsubscribed.\r\n     */\r\n    delayClean?: boolean;\r\n}\r\n\r\n/**\r\n * Function to create a new observable with specified parameters.\r\n */\r\nexport type QueryCacheCreator<TData, TParams> = (param: TParams) => Observable<TData>;\r\n\r\n/**\r\n * Function to generate serialized string from specified parameters.\r\n */\r\nexport type QueryCacheSerializeParams<TParams> = (param: TParams) => string;\r\n\r\n/**\r\n * Function to destroy any remained condition to clear up.\r\n */\r\nexport type QueryCacheDestroy = () => void;\r\n\r\n/**\r\n * Query cache context for a subscription.\r\n */\r\ninterface QueryCacheContext<TData> {\r\n    subscription: Subscription;\r\n    next: (value: TData) => void;\r\n    error: (error: any) => void;\r\n    complete: () => void;\r\n    unsubscribe: () => void;\r\n    errorCount: number;\r\n    unsubscribeCount: number;\r\n}\r\n\r\n/**\r\n * Query cache data interface.\r\n */\r\ninterface QueryCachedData<TData, TParams> {\r\n    fetch: Subject<QueryCacheOptions<TParams>>;\r\n    refresh: Subject<number>;\r\n    apply: Subject<TData>;\r\n    publish: Observable<TData>;\r\n    subscribers: QueryCacheContext<TData>[];\r\n}\r\n\r\n/**\r\n * Cycle repeater context interface.\r\n */\r\ninterface RepeaterContext {\r\n    /**\r\n     * Current cycle counter value.\r\n     */\r\n    counter: number;\r\n\r\n    /**\r\n     * Requested cycle counter to trigger.\r\n     */\r\n    request: number;\r\n\r\n    /**\r\n     * Cycle interval millisecond.\r\n     */\r\n    pulse: number;\r\n\r\n    /**\r\n     * One cycle number.\r\n     */\r\n    cycle: number;\r\n\r\n    /**\r\n     * Validation counter.\r\n     */\r\n    verify: number;\r\n\r\n    /**\r\n     * setInterval timer object.\r\n     */\r\n    timer: any;\r\n}\r\n\r\n/**\r\n * Query Cache class.\r\n * - Create a cache entry by \"create\" call with creator object.\r\n * - Subscribe with options to get query result.\r\n * - Refresh data on-demand and interval.\r\n * - Dispose the resource when it's done.\r\n * - Recover and re-subscribe with the same handlers if necessary after an error.\r\n *\r\n * TData the data type of observable responds.\r\n * TParams the options parameters to pass the creator to create new observable.\r\n */\r\nexport class QueryCache<TData, TParams> {\r\n    /**\r\n     * Internal instance id counter.\r\n     * (set id number to be 0 to bring back older behavior)\r\n     */\r\n    private static id = 10000;\r\n\r\n    /**\r\n     * Delay clean up time (10 seconds)\r\n     */\r\n    private static delayTime: number = 10 * 1000;\r\n\r\n    /**\r\n     * Repeater is parked state.\r\n     */\r\n    private static repeaterParked = -1;\r\n\r\n    /**\r\n     * Cached data collection with observable ReplaySubject.\r\n     */\r\n    private cachedData: QueryCachedData<TData, TParams>;\r\n\r\n    /**\r\n     * Current options, passed from the fetch call.\r\n     */\r\n    private options: QueryCacheOptions<TParams>;\r\n\r\n    /**\r\n     * Current observer of create observable.\r\n     */\r\n    private observer: Observer<TData>;\r\n\r\n    /**\r\n     * Key string serialized by TParams.\r\n     */\r\n    private key: string;\r\n\r\n    /**\r\n     * Delay clean timer object.\r\n     */\r\n    private delayTimer: any;\r\n\r\n    /**\r\n     * Auto fetch options.\r\n     */\r\n    private autoFetchOptions: QueryCacheOptions<TParams>;\r\n\r\n    /**\r\n     * Repeater context to cycle repeating observable.\r\n     */\r\n    private repeaterContext: RepeaterContext;\r\n\r\n    /**\r\n     * Instance id of query cache for internal debugging.\r\n     */\r\n    private id = '{0}-{1}'.format(MsftSme.self().Init.moduleName, ++QueryCache.id);\r\n\r\n    /**\r\n     * The tracing name.\r\n     */\r\n    private name: string;\r\n\r\n    /**\r\n     * The start time of tracing.\r\n     */\r\n    private traceTime: number;\r\n\r\n    /**\r\n     * Initializes a new instance of the QueryCache.\r\n     *\r\n     * @param create the function to create a new observable with specified parameters.\r\n     * @param serializeParams the function to generate serialized string from specified parameters. (optional)\r\n     * @param destroy the function to clean up any residue after all reference was gone. (optional)\r\n     */\r\n    constructor(\r\n        private create: QueryCacheCreator<TData, TParams>,\r\n        private serializeParams?: QueryCacheSerializeParams<TParams>,\r\n        private destroy?: QueryCacheDestroy) {\r\n    }\r\n\r\n    /**\r\n     * Use the QueryCache inside of Worker or background tab view.\r\n     * setInterval time lost accuracy within background. Use setTimeout recursive query by expand observable operator.\r\n     * Call the static function once before creating new instance of QueryCache, probably set at app.component.ts file.\r\n     */\r\n    public static useExpandInterval(): void {\r\n        QueryCache.id = 0;\r\n    }\r\n\r\n    /**\r\n     * Create or return the cached observable.\r\n     *\r\n     * @param autoFetchOptions the options parameter auto-fetch when subscribe is called.\r\n     * @return Observable<TData> the observable object.\r\n     */\r\n    public createObservable(autoFetchOptions?: QueryCacheOptions<TParams>): Observable<TData> {\r\n        // for recovery scenario, this.cachedData.subscribers is retained.\r\n        // clear them here if no publish ever made.\r\n        if (this.cachedData && this.cachedData.publish) {\r\n            if (autoFetchOptions) {\r\n                // schedule the fetch immediately after current call stack.\r\n                setTimeout(() => {\r\n                    // cancel auto fetch if already unsubscribed.\r\n                    if (this.cachedData && this.cachedData.publish) {\r\n                        this.fetch(autoFetchOptions);\r\n                    }\r\n                });\r\n            }\r\n\r\n            return this.cachedData.publish;\r\n        }\r\n\r\n        const fetch = new Subject<QueryCacheOptions<TParams>>();\r\n        const refresh = new Subject<number>();\r\n        const apply = new Subject<TData>();\r\n        const subscribers = [];\r\n        const publish: Observable<TData> = this.createPublishObservable(fetch, refresh, apply, subscribers);\r\n        this.cachedData = <QueryCachedData<TData, TParams>>{ fetch, refresh, publish, apply, subscribers };\r\n        this.autoFetchOptions = autoFetchOptions;\r\n        return publish;\r\n    }\r\n\r\n    /**\r\n     * Unsubscribe any remained subscribers and dispose all remained resources.\r\n     * - clearCache() is not necessary to be called if all subscriptions are unsubscribe'ed properly.\r\n     */\r\n    public clearCache(): void {\r\n        if (!this.cachedData) {\r\n            return;\r\n        }\r\n\r\n        if (this.cachedData.subscribers) {\r\n            const tempSubscribers = this.cachedData.subscribers.slice(0);\r\n            for (const context of tempSubscribers) {\r\n                context.subscription.unsubscribe();\r\n            }\r\n        }\r\n\r\n        this.cleanup(false);\r\n    }\r\n\r\n    /**\r\n     * Fetch with new query options.\r\n     * - The fetch starts new query when cache is empty, current cache doesn't match the key generated by\r\n     *   serializedParams function which was configured at the constructor, interval was changed (ex.\r\n     *   changing zero interval to active interval or other way around), or the first subscriber like when\r\n     *   delayClean comes back active subscription.\r\n     *\r\n     * @param options the options with interval, delay clean, recovery and parameters.\r\n     */\r\n    public fetch(options: QueryCacheOptions<TParams>): void {\r\n        if (!this.cachedData) {\r\n            const message = MsftSme.getStrings<Strings>().MsftSmeShell.Core.Error.QueryCacheFetchOrder.message;\r\n            Logging.logError('QueryCache', message);\r\n            throw new Error(message);\r\n        }\r\n\r\n        if (this.cachedData.fetch) {\r\n            // send new request when cache doesn't match the key, interval was changed, or single subscriber.\r\n            const key = this.serializeParams ? this.serializeParams(options.params) : '';\r\n            if (!this.options\r\n                || this.key !== key\r\n                || this.options.interval !== options.interval\r\n                || this.key == null) {\r\n                this.key = key;\r\n                this.cachedData.fetch.next(options);\r\n            }\r\n        } else {\r\n            Logging.logWarning(\r\n                'QueryCache',\r\n                MsftSme.getStrings<Strings>().MsftSmeShell.Core.Error.QueryCacheFetchErrorOnce.message);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Refresh the query cache with last options and parameters provided on the fetch call.\r\n     */\r\n    public refresh(): void {\r\n        if (!this.cachedData) {\r\n            const message = MsftSme.getStrings<Strings>().MsftSmeShell.Core.Error.QueryCacheRefreshOrder.message;\r\n            Logging.logError('QueryCache', message);\r\n            throw new Error(message);\r\n        }\r\n\r\n        if (this.cachedData.refresh) {\r\n            this.cachedData.refresh.next(undefined);\r\n        } else {\r\n            Logging.logWarning(\r\n                'QueryCache',\r\n                MsftSme.getStrings<Strings>().MsftSmeShell.Core.Error.QueryCacheRefreshErrorOnce.message);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Recover the observable and subscription.\r\n     * - Recover can be used to resubscribe when the observable got any error situation. The observable would\r\n     *   be unsubscribed state when it got an error response, this function allows to resubscribe without\r\n     *   recreate observable and subscription.\r\n     *\r\n     * @param autoFetch if true auto fetch after re-subscribed.\r\n     */\r\n    public recover(autoFetch: boolean): void {\r\n        if (!this.cachedData) {\r\n            const message = MsftSme.getStrings<Strings>().MsftSmeShell.Core.Error.QueryCacheRecoverNoCachedResource.message;\r\n            Logging.logError('QueryCache', message);\r\n            throw new Error(message);\r\n        }\r\n\r\n        if (!this.options || !this.options.enableRecovery) {\r\n            const message = MsftSme.getStrings<Strings>().MsftSmeShell.Core.Error.QueryCacheRecoverMissingRecoveryOption.message;\r\n            Logging.logError('QueryCache', message);\r\n            throw new Error(message);\r\n        }\r\n\r\n        const tempCachedData = this.cachedData;\r\n        if (tempCachedData.refresh) {\r\n            tempCachedData.refresh.complete();\r\n        }\r\n\r\n        if (tempCachedData.fetch) {\r\n            tempCachedData.fetch.complete();\r\n        }\r\n\r\n        if (tempCachedData.apply) {\r\n            tempCachedData.apply.complete();\r\n        }\r\n\r\n        for (const context of this.cachedData.subscribers) {\r\n            // call original unsubscribe function.\r\n            context.subscription.unsubscribe();\r\n        }\r\n\r\n        // recreate new publish observable and subscribe to original set of handlers.\r\n        const tempSubscribers = this.cachedData.subscribers.slice(0);\r\n\r\n        const fetch = new Subject<QueryCacheOptions<TParams>>();\r\n        const refresh = new Subject<number>();\r\n        const apply = new Subject<TData>();\r\n        const subscribers = [];\r\n        const publish: Observable<TData> = this.createPublishObservable(fetch, refresh, apply, subscribers);\r\n        this.cachedData = <QueryCachedData<TData, TParams>>{ fetch, refresh, publish, apply, subscribers };\r\n        for (const context of tempSubscribers) {\r\n            (<any>this.cachedData.publish).subscribe(context.next, context.error, context.complete, context.subscription);\r\n        }\r\n\r\n        if (autoFetch) {\r\n            fetch.next(this.options);\r\n        } else {\r\n            this.options = {};\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Apply instant data to the query cache. The data will be delivered to the subscriber immediately.\r\n     *\r\n     * @param data the data to apply to the replay.\r\n     */\r\n    public apply(data: TData): void {\r\n        if (this.cachedData.apply) {\r\n            this.cachedData.apply.next(data);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Enable tracing of this query cache instance.\r\n     *\r\n     * @param name the name of query cache.\r\n     */\r\n    public trace(name: string): void {\r\n        this.name = name;\r\n        this.traceTime = Date.now();\r\n        this.log(`tracing ${this.name}: ${this.id}`);\r\n    }\r\n\r\n    private log(message: string): void {\r\n        const time = ((Date.now() - this.traceTime)) / 1000;\r\n        Logging.debug(`QC: ${this.id} ${this.name}: ${time}sec: ${message}`);\r\n    }\r\n\r\n    private createPublishObservable(\r\n            fetch: Subject<QueryCacheOptions<TParams>>,\r\n            refresh: Subject<number>,\r\n            apply: Subject<TData>,\r\n            subscribers: QueryCacheContext<TData>[]): Observable<TData> {\r\n        const publish: Observable<TData> =\r\n                // start data query when new fetch is requested.\r\n                fetch\r\n                    .pipe(\r\n                        // switch map unsubscribe previous fetch observable tree, and re-create new one.\r\n                        switchMap(options => {\r\n                            // remember last options.\r\n                            this.options = options || {};\r\n                            // merge output from expand-delay object and refresh object.\r\n                            return merge<[TData, TData, TData]>(\r\n                                // submit initial query.\r\n                                this.repeat(),\r\n                                // refresh to trigger new observable.\r\n                                refresh.pipe(switchMap(() => this.create(this.options.params))),\r\n                                // apply data.\r\n                                apply\r\n                            );\r\n                        }),\r\n                        // multicast the result so multiple subscribers can share.\r\n                        // and Replay last result if later subscriber looks for the result.\r\n                        multicast(new ReplaySubject(1)),\r\n                        // keep the reference count so publish observable active.\r\n                        refCount());\r\n\r\n        // override subscribe call to keep track subscription.\r\n        publish.subscribe = <any>((next, error, complete, recoverSubscription) => {\r\n            if (this.delayTimer) {\r\n                clearTimeout(this.delayTimer);\r\n                this.delayTimer = null;\r\n                if (this.options && this.options.interval) {\r\n                    this.options.interval = null;\r\n                }\r\n            }\r\n\r\n            // override default handler with No-OP call if not set.\r\n            // this allows all subscribe call to be get called when an error reported.\r\n            next = next || MsftSme.noop;\r\n            error = error || MsftSme.noop;\r\n\r\n            const context = <QueryCacheContext<TData>>{ next, error, complete, errorCount: 0, unsubscribeCount: 0 };\r\n            const hookError = (errorData) => {\r\n                context.errorCount++;\r\n                error(errorData);\r\n            };\r\n            context.subscription = Object.getPrototypeOf(publish).subscribe.call(publish, next, hookError, complete);\r\n\r\n            // hook up old subscription so old subscriber can unsubscribe properly.\r\n            if (recoverSubscription) {\r\n                recoverSubscription.unsubscribe = () => context.subscription.unsubscribe();\r\n            }\r\n\r\n            // add subscribers to the inventory before adding to the un-subscription list. add() could call unsubscribe immediately.\r\n            subscribers.push(context);\r\n\r\n            // add internal unsubscribe to the original subscription.\r\n            context.subscription.add(this.internalUnsubscribe.bind(this, context));\r\n\r\n            // if createObservable() is called with autoFetchOptions, it start fetching data immediately when subscribe() is called.\r\n            if (this.autoFetchOptions) {\r\n                this.fetch(this.autoFetchOptions);\r\n                this.autoFetchOptions = null;\r\n            }\r\n\r\n            if (this.name) {\r\n                this.log(`subscribe count=${subscribers.length}`);\r\n            }\r\n\r\n            return context.subscription;\r\n        });\r\n\r\n        return publish;\r\n    }\r\n\r\n    private internalUnsubscribe(context: QueryCacheContext<TData>): void {\r\n        context.unsubscribeCount++;\r\n        if (context.unsubscribeCount > 1) {\r\n            // ignore if it's called twice and more.\r\n            return;\r\n        }\r\n\r\n        if (!this.cachedData) {\r\n            return;\r\n        }\r\n\r\n        const options = this.options || {};\r\n        if (options.enableRecovery) {\r\n            // indicating normal unsubscribe call, so delete it.\r\n            if (context.errorCount === 0) {\r\n                const contextIndex = this.cachedData.subscribers.indexOf(context);\r\n                if (contextIndex >= 0) {\r\n                    this.cachedData.subscribers.splice(contextIndex, 1);\r\n                }\r\n            }\r\n\r\n            // on the recovery mode, retains all subscriber context, and don't clean up.\r\n            // if there no active subscription, make cleanup call.\r\n            const findAny = this.cachedData.subscribers.find(item => !item.subscription.closed);\r\n            if (!findAny) {\r\n                if (options.delayClean) {\r\n                    this.delayTimer = setTimeout(\r\n                        () => {\r\n                            this.cleanup(true);\r\n                            this.delayTimer = null;\r\n                        },\r\n                        QueryCache.delayTime);\r\n                } else {\r\n                    this.cleanup(true);\r\n                }\r\n            }\r\n\r\n            return;\r\n        }\r\n\r\n        // non recovery mode.\r\n        const index = this.cachedData.subscribers.indexOf(context);\r\n        if (index >= 0) {\r\n            this.cachedData.subscribers.splice(index, 1);\r\n        }\r\n\r\n        if (this.cachedData.subscribers.length === 0) {\r\n            if (options.delayClean) {\r\n                this.delayTimer = setTimeout(\r\n                    () => {\r\n                        this.cleanup(false);\r\n                        this.delayTimer = null;\r\n                    },\r\n                    QueryCache.delayTime);\r\n            } else {\r\n                this.cleanup(false);\r\n            }\r\n        }\r\n    }\r\n\r\n    private cleanup(recovery: boolean): void {\r\n        if (this.name) {\r\n            this.log('unsubscribed');\r\n        }\r\n\r\n        this.repeaterStop();\r\n\r\n        if (!this.cachedData) {\r\n            return;\r\n        }\r\n\r\n        if (this.cachedData.fetch) {\r\n            this.cachedData.fetch.complete();\r\n            this.cachedData.fetch = null;\r\n        }\r\n\r\n        if (this.cachedData.refresh) {\r\n            this.cachedData.refresh.complete();\r\n            this.cachedData.refresh = null;\r\n        }\r\n\r\n        if (this.cachedData.apply) {\r\n            this.cachedData.apply.complete();\r\n            this.cachedData.apply = null;\r\n        }\r\n\r\n        this.cachedData.publish = null;\r\n        this.key = null;\r\n\r\n        // on recovery cleanup, retain subscribers and options.\r\n        if (!recovery) {\r\n            this.cachedData.subscribers = null;\r\n            this.cachedData = null;\r\n            this.options = {};\r\n        }\r\n\r\n        if (this.destroy) {\r\n            this.destroy();\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Repeat observable.\r\n     * Using free running interval to avoid setTimeout recursive or Observable.expand recursive stacks.\r\n     * It reduces the usage of browser memory but use more frequent timer event as pulse in the code.\r\n     */\r\n    private repeat(): Observable<TData> {\r\n        if (QueryCache.id < 10000) {\r\n            // Legacy code pattern which use expand/recursive/setTimeout\r\n            // submit initial query.\r\n            return this.create(this.options.params)\r\n                // expand to re-query next interval\r\n                .pipe(\r\n                    expand((result) => {\r\n                        if (!this.options.interval || this.options.interval <= 0) {\r\n                            // stop the interval delay.\r\n                            return EMPTY;\r\n                        }\r\n                        // return new observable after the delay.\r\n                        return of(result)\r\n                            .pipe(\r\n                                // delay for the interval.\r\n                                delay(this.options.interval),\r\n                                // complete previous observable and switch to new observable.\r\n                                switchMap(() => this.create(this.options.params)));\r\n                    }));\r\n        }\r\n\r\n        this.repeaterStop();\r\n        if (!this.options.interval) {\r\n            // single observable. (no repeat)\r\n            return this.create(this.options.params);\r\n        }\r\n\r\n        // create new observable to repeat calling repeaterCreate() function.\r\n        return new Observable(\r\n            (observer: Observer<TData>) => {\r\n                this.observer = observer;\r\n                this.repeaterInitialize();\r\n                this.repeaterStart();\r\n                return () => {\r\n                    // either closed by unsubscribe or observer is completed.\r\n                    this.observer?.complete();\r\n\r\n                    this.observer = null;\r\n                    this.repeaterStop();\r\n                };\r\n            });\r\n    }\r\n\r\n    private repeaterInitialize(): void {\r\n        if (this.options.interval < 1000) {\r\n            // min 1 sec interval.\r\n            this.options.interval = 1000;\r\n        }\r\n\r\n        // 100 or 200 or 1000 pulse.\r\n        const pulse = this.options.interval <= 5000 ? 100 : (this.options.interval <= 10000 ? 200 : 1000);\r\n        const cycle = Math.floor(this.options.interval / pulse);\r\n        this.repeaterContext = {\r\n            counter: 0,\r\n            request: QueryCache.repeaterParked,\r\n            timer: 0,\r\n            pulse,\r\n            cycle,\r\n            verify: 0\r\n        };\r\n    }\r\n\r\n    private repeaterStart(): void {\r\n        // run first query.\r\n        this.repeaterCreate();\r\n\r\n        // start interval counter is incremented but cycled by cycle number.\r\n        // if it hits request === counter, call this.repeaterCreate();\r\n        this.repeaterContext.timer = setInterval(this.repeaterCheck.bind(this), this.repeaterContext.pulse);\r\n    }\r\n\r\n    private repeaterStop(): void {\r\n        if (!this.repeaterContext) {\r\n            return;\r\n        }\r\n\r\n        if (this.repeaterContext.timer) {\r\n            clearInterval(this.repeaterContext.timer);\r\n            this.repeaterContext.timer = null;\r\n        }\r\n\r\n        this.repeaterContext.request = QueryCache.repeaterParked;\r\n        this.repeaterContext.verify = 0;\r\n        this.repeaterContext.counter = 0;\r\n    }\r\n\r\n    private repeaterCreate(): void {\r\n        if (!this.observer) {\r\n            this.repeaterStop();\r\n            return;\r\n        }\r\n\r\n        if (!this.options.interval) {\r\n            if (this.observer) {\r\n                this.observer.complete();\r\n                this.observer = null;\r\n            }\r\n\r\n            this.repeaterStop();\r\n            return;\r\n        }\r\n\r\n        // subscribe to the create observable with params passed.\r\n        this.create(this.options.params)\r\n            .subscribe({\r\n                next: data => {\r\n                    this.observer?.next(data);\r\n                },\r\n                error: error => {\r\n                    this.observer?.error(error);\r\n                    this.repeaterStop();\r\n                },\r\n                complete: () => {\r\n                    if (!this.options.interval) {\r\n                        // complete if the interval was reset.\r\n                        if (this.observer) {\r\n                            this.observer.complete();\r\n                            this.observer = null;\r\n                        }\r\n\r\n                        this.repeaterStop();\r\n                        return;\r\n                    }\r\n\r\n                    if (this.name) {\r\n                        this.log(`repeat: request=${this.repeaterContext.request}, counter=${this.repeaterContext.counter}`);\r\n                    }\r\n\r\n                    // set next request counter number if it's parked.\r\n                    if (this.repeaterContext.request === QueryCache.repeaterParked) {\r\n                        this.repeaterContext.request = this.repeaterContext.counter;\r\n                    }\r\n                }\r\n            });\r\n    }\r\n\r\n    private repeaterCheck(): void {\r\n        if (this.repeaterContext.request === QueryCache.repeaterParked) {\r\n            return;\r\n        }\r\n\r\n        // update cycle counter\r\n        this.repeaterContext.counter = ++this.repeaterContext.counter % this.repeaterContext.cycle;\r\n\r\n        // update verify counter which starts from 0 to cycle.\r\n        this.repeaterContext.verify++;\r\n\r\n        // check if it hits a cycle.\r\n        if (this.repeaterContext.counter === this.repeaterContext.request) {\r\n            // wait for repeatCreate observable to complete. reset verify counter first before activate the observable.\r\n            this.repeaterContext.request = QueryCache.repeaterParked;\r\n            this.repeaterContext.verify = 0;\r\n            this.repeaterCreate();\r\n        } else if (this.repeaterContext.verify > this.repeaterContext.cycle + 1) {\r\n            throw new Error('QueryCache: Cycle counter running over. cycle={0}, counter={1}, request={2}, verify={3}'.format(\r\n                this.repeaterContext.cycle, this.repeaterContext.counter, this.repeaterContext.request, this.repeaterContext.verify));\r\n        }\r\n    }\r\n}\r\n"]}