"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.syncState = syncState; exports.syncStates = syncStates; var _rxjs = require("rxjs"); var _operators = require("rxjs/operators"); var _fastDeepEqual = _interopRequireDefault(require("fast-deep-equal")); var _common = require("../../common"); var _diff_object = require("../state_management/utils/diff_object"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0 and the Server Side Public License, v 1; you may not use this file except * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ /** * Utility for syncing application state wrapped in state container * with some kind of storage (e.g. URL) * * Go {@link https://github.com/elastic/kibana/tree/main/src/plugins/kibana_utils/docs/state_sync | here} for a complete guide and examples. * * @example * * the simplest use case * ```ts * const stateStorage = createKbnUrlStateStorage(); * syncState({ * storageKey: '_s', * stateContainer, * stateStorage * }); * ``` * * @example * conditionally configuring sync strategy * ```ts * const stateStorage = createKbnUrlStateStorage({useHash: config.get('state:stateContainerInSessionStorage')}) * syncState({ * storageKey: '_s', * stateContainer, * stateStorage * }); * ``` * * @example * implementing custom sync strategy * ```ts * const localStorageStateStorage = { * set: (storageKey, state) => localStorage.setItem(storageKey, JSON.stringify(state)), * get: (storageKey) => localStorage.getItem(storageKey) ? JSON.parse(localStorage.getItem(storageKey)) : null * }; * syncState({ * storageKey: '_s', * stateContainer, * stateStorage: localStorageStateStorage * }); * ``` * * @example * transforming state before serialising * Useful for: * * Migration / backward compatibility * * Syncing part of state * * Providing default values * ```ts * const stateToStorage = (s) => ({ tab: s.tab }); * syncState({ * storageKey: '_s', * stateContainer: { * get: () => stateToStorage(stateContainer.get()), * set: stateContainer.set(({ tab }) => ({ ...stateContainer.get(), tab }), * state$: stateContainer.state$.pipe(map(stateToStorage)) * }, * stateStorage * }); * ``` * * @param - syncing config {@link IStateSyncConfig} * @returns - {@link ISyncStateRef} * @public */ function syncState({ storageKey, stateStorage, stateContainer }) { const subscriptions = []; const updateState = () => { const newState = stateStorage.get(storageKey); const oldState = stateContainer.get(); if (newState) { // apply only real differences to new state const mergedState = { ...oldState }; // merges into 'mergedState' all differences from newState, // but leaves references if they are deeply the same const diff = (0, _diff_object.applyDiff)(mergedState, newState); if (diff.keys.length > 0) { stateContainer.set(mergedState); } } else if (oldState !== newState) { // empty new state case stateContainer.set(newState); } }; const updateStorage = () => { const newStorageState = stateContainer.get(); const oldStorageState = stateStorage.get(storageKey); if (!(0, _fastDeepEqual.default)(newStorageState, oldStorageState)) { stateStorage.set(storageKey, newStorageState); } }; const onStateChange$ = stateContainer.state$.pipe((0, _common.distinctUntilChangedWithInitialValue)(stateContainer.get(), _fastDeepEqual.default), (0, _operators.tap)(() => updateStorage())); const onStorageChange$ = stateStorage.change$ ? stateStorage.change$(storageKey).pipe((0, _common.distinctUntilChangedWithInitialValue)(stateStorage.get(storageKey), _fastDeepEqual.default), (0, _operators.tap)(() => { updateState(); })) : _rxjs.EMPTY; return { stop: () => { // if stateStorage has any cancellation logic, then run it if (stateStorage.cancel) { stateStorage.cancel(); } subscriptions.forEach(s => s.unsubscribe()); subscriptions.splice(0, subscriptions.length); }, start: () => { if (subscriptions.length > 0) { throw new Error("syncState: can't start syncing state, when syncing is in progress"); } subscriptions.push(onStateChange$.subscribe(), onStorageChange$.subscribe()); } }; } /** * @example * sync multiple different sync configs * ```ts * syncStates([ * { * storageKey: '_s1', * stateStorage: stateStorage1, * stateContainer: stateContainer1, * }, * { * storageKey: '_s2', * stateStorage: stateStorage2, * stateContainer: stateContainer2, * }, * ]); * ``` * @param stateSyncConfigs - Array of {@link IStateSyncConfig} to sync */ function syncStates(stateSyncConfigs) { const syncRefs = stateSyncConfigs.map(config => syncState(config)); return { stop: () => { syncRefs.forEach(s => s.stop()); }, start: () => { syncRefs.forEach(s => s.start()); } }; }