import { isFunction, internal, syncState, when, observable, isPromise as isPromise$1, mergeIntoObservable, isEmpty, endBatch, beginBatch, isString, isNullOrUndefined, isObject, isDate, setAtPath, constructObjectWithPath, applyChanges, shouldIgnoreUnobserved, whenReady, trackSelector, isPlainObject, ObservableHint, isArray } from "./index.js";
var observableSyncConfiguration = {};
function removeNullUndefined(a, recursive) {
  const out = {};
  Object.keys(a).forEach((key) => {
    if (a[key] !== null && a[key] !== void 0) {
      out[key] = a[key];
    }
  });
  return out;
}
function deepEqual(a, b, ignoreFields, nullVsUndefined) {
  if (a === b)
    return true;
  if (isNullOrUndefined(a) !== isNullOrUndefined(b))
    return false;
  if (!isObject(a) || !isObject(b))
    return a === b;
  const keysA = Object.keys(a).filter((key) => true);
  const keysB = Object.keys(b).filter((key) => true);
  if (keysA.length !== keysB.length)
    return false;
  return keysA.every((key) => {
    if (!Object.prototype.hasOwnProperty.call(b, key))
      return false;
    if (isDate(a[key]) && isDate(b[key])) {
      return a[key].getTime() === b[key].getTime();
    }
    return deepEqual(a[key], b[key]);
  });
}
function isPromise(obj) {
  return obj instanceof Promise;
}
function calculateRetryDelay(retryOptions, retryNum) {
  const { backoff, delay = 1e3, infinite, times = 3, maxDelay = 3e4 } = retryOptions;
  if (infinite || retryNum < times) {
    const delayTime = Math.min(delay * (backoff === "constant" ? 1 : 2 ** retryNum), maxDelay);
    return delayTime;
  }
  return null;
}
function createRetryTimeout(retryOptions, retryNum, fn) {
  const delayTime = calculateRetryDelay(retryOptions, retryNum);
  if (delayTime) {
    return setTimeout(fn, delayTime);
  } else {
    return false;
  }
}
var mapRetryTimeouts = /* @__PURE__ */ new Map();
function runWithRetry(state, retryOptions, retryId, fn) {
  try {
    let value = fn(state);
    if (isPromise(value) && retryOptions) {
      let timeoutRetry;
      if (mapRetryTimeouts.has(retryId)) {
        clearTimeout(mapRetryTimeouts.get(retryId));
      }
      return new Promise((resolve, reject) => {
        const run = () => {
          value.then((val) => {
            resolve(val);
          }).catch((error) => {
            state.retryNum++;
            if (timeoutRetry) {
              clearTimeout(timeoutRetry);
            }
            if (!state.cancelRetry) {
              const timeout = createRetryTimeout(retryOptions, state.retryNum, () => {
                value = fn(state);
                run();
              });
              if (timeout === false) {
                state.cancelRetry = true;
                reject(error);
              } else {
                mapRetryTimeouts.set(retryId, timeout);
                timeoutRetry = timeout;
              }
            }
          });
        };
        run();
      });
    }
    return value;
  } catch (error) {
    return Promise.reject(error);
  }
}
async function waitForSet(waitForSet2, changes, value, params = {}) {
  const waitFn = isFunction(waitForSet2) ? waitForSet2({ changes, value, ...params }) : waitForSet2;
  if (waitFn) {
    await when(waitFn);
  }
}
var { clone } = internal;
function createRevertChanges(obs$, changes) {
  return () => {
    const previous = applyChanges(
      clone(obs$.peek()),
      changes,
      /*applyPrevious*/
      true
    );
    onChangeRemote2(() => {
      obs$.set(previous);
    });
  };
}
var { clone: clone2, deepMerge, getNode, getNodeValue, getValueAtPath, globalState, symbolLinked, createPreviousHandler } = internal;
var mapSyncPlugins = /* @__PURE__ */ new WeakMap();
var allSyncStates = /* @__PURE__ */ new Map();
var metadatas = /* @__PURE__ */ new WeakMap();
var promisesLocalSaves = /* @__PURE__ */ new Set();
function parseLocalConfig(config) {
  return config ? isString(config) ? { table: config, config: { name: config } } : { table: config.name, config } : {};
}
function doInOrder(arg1, arg2) {
  return isPromise$1(arg1) ? arg1.then(arg2) : arg2(arg1);
}
function onChangeRemote2(cb) {
  endBatch(true);
  globalState.isLoadingRemote = true;
  beginBatch();
  cb();
  globalState.isLoadingRemote = false;
  endBatch(true);
}
async function transformSaveData(value, path, pathTypes, { transform }) {
  if (transform == null ? void 0 : transform.save) {
    const constructed = constructObjectWithPath(path, pathTypes, value);
    const saved = await transform.save(constructed);
    value = saved;
    const outPath = [];
    for (let i = 0; i < path.length; i++) {
      outPath[i] = Object.keys(value)[0];
      value = value[outPath[i]];
    }
    path = outPath;
  }
  return { value, path };
}
function transformLoadData(value, { transform }, doUserTransform, method) {
  if (doUserTransform && (transform == null ? void 0 : transform.load)) {
    value = transform.load(value, method);
  }
  return value;
}
async function updateMetadataImmediate(value$, localState, syncState2, syncOptions, newMetadata) {
  const saves = Array.from(promisesLocalSaves);
  if (saves.length > 0) {
    await Promise.all(saves);
  }
  const { pluginPersist } = localState;
  const { table, config } = parseLocalConfig(syncOptions.persist);
  const oldMetadata = metadatas.get(value$);
  const { lastSync } = newMetadata;
  const metadata = Object.assign({}, oldMetadata, newMetadata);
  metadatas.set(value$, metadata);
  if (pluginPersist) {
    await pluginPersist.setMetadata(table, metadata, config);
  }
  if (lastSync) {
    syncState2.assign({
      lastSync
    });
  }
}
function updateMetadata(value$, localState, syncState2, syncOptions, newMetadata) {
  if (localState.timeoutSaveMetadata) {
    clearTimeout(localState.timeoutSaveMetadata);
  }
  metadatas.set(value$, { ...metadatas.get(value$) || {}, ...newMetadata });
  localState.timeoutSaveMetadata = setTimeout(() => {
    updateMetadataImmediate(value$, localState, syncState2, syncOptions, metadatas.get(value$));
  }, 0);
}
var _queuedChanges = [];
var _queuedRemoteChanges = /* @__PURE__ */ new Map();
var _queuedRemoteChangesTimeouts = /* @__PURE__ */ new Map();
function mergeChanges(changes) {
  const changesByPath = /* @__PURE__ */ new Map();
  const changesOut = [];
  for (let i = 0; i < changes.length; i++) {
    const change = changes[i];
    const pathStr = change.path.join("/");
    const existing = changesByPath.get(pathStr);
    if (existing) {
      if (change.valueAtPath === existing.prevAtPath) {
        changesOut.splice(changesOut.indexOf(change), 1);
      } else {
        existing.valueAtPath = change.valueAtPath;
      }
    } else {
      let found = false;
      for (let u = 0; u < change.path.length; u++) {
        const path = change.path.slice(0, u).join("/");
        if (changesByPath.has(path)) {
          const remaining = change.path.slice(u);
          setAtPath(
            changesByPath.get(path).valueAtPath,
            remaining,
            change.pathTypes.slice(u),
            change.valueAtPath
          );
          found = true;
          break;
        }
      }
      if (!found) {
        changesByPath.set(pathStr, change);
        changesOut.push(change);
      }
    }
  }
  return changesOut;
}
function mergeQueuedChanges(allChanges) {
  const changesByOptionsRemote = /* @__PURE__ */ new Map();
  const changesByOptionsLocal = /* @__PURE__ */ new Map();
  const outRemote = /* @__PURE__ */ new Map();
  const outLocal = /* @__PURE__ */ new Map();
  for (let i = 0; i < allChanges.length; i++) {
    const value = allChanges[i];
    const { changes, inRemoteChange, syncOptions } = value;
    const targetMap = inRemoteChange ? outRemote : outLocal;
    const changesMap = inRemoteChange ? changesByOptionsRemote : changesByOptionsLocal;
    const existing = changesMap.get(syncOptions);
    const newChanges = existing ? [...existing, ...changes] : changes;
    const merged = mergeChanges(newChanges);
    changesMap.set(syncOptions, merged);
    value.changes = merged;
    targetMap.set(syncOptions, value);
  }
  return Array.from(outRemote.values()).concat(Array.from(outLocal.values()));
}
async function processQueuedChanges() {
  var _a, _b;
  const queuedChanges = mergeQueuedChanges(_queuedChanges);
  _queuedChanges = [];
  const pendingSyncOptions = /* @__PURE__ */ new Set();
  for (let i = 0; i < queuedChanges.length; i++) {
    const change = queuedChanges[i];
    if (!change.inRemoteChange) {
      if (!_queuedRemoteChanges.has(change.syncOptions)) {
        _queuedRemoteChanges.set(change.syncOptions, []);
      }
      pendingSyncOptions.add(change.syncOptions);
      _queuedRemoteChanges.get(change.syncOptions).push(change);
    }
  }
  const preppedChangesLocal = await Promise.all(queuedChanges.map(prepChangeLocal));
  await Promise.all(queuedChanges.map(prepChangeRemote));
  await Promise.all(preppedChangesLocal.map(doChangeLocal));
  for (const options of pendingSyncOptions) {
    const timeout = (_b = options.debounceSet) != null ? _b : (_a = observableSyncConfiguration) == null ? void 0 : _a.debounceSet;
    const timeoutSaveRemote = _queuedRemoteChangesTimeouts.get(options);
    const run = () => processQueuedRemoteChanges(options);
    if (timeout) {
      if (timeoutSaveRemote) {
        clearTimeout(timeoutSaveRemote);
      }
      _queuedRemoteChangesTimeouts.set(options, setTimeout(run, timeout));
    } else {
      run();
    }
  }
}
async function processQueuedRemoteChanges(syncOptions) {
  const arr = _queuedRemoteChanges.get(syncOptions);
  if (arr == null ? void 0 : arr.length) {
    const queuedRemoteChanges = mergeQueuedChanges(arr);
    _queuedRemoteChanges.set(syncOptions, []);
    const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote));
    preppedChangesRemote.forEach(doChangeRemote);
  }
}
async function prepChangeLocal(queuedChange) {
  const { syncState: syncState2, changes, syncOptions, inRemoteChange, isApplyingPending } = queuedChange;
  const persist = syncOptions.persist;
  const { config: configLocal } = parseLocalConfig(persist);
  const saveLocal = (persist == null ? void 0 : persist.name) && !configLocal.readonly && !isApplyingPending && syncState2.isPersistEnabled.peek();
  const saveRemote = !!(!inRemoteChange && (syncOptions == null ? void 0 : syncOptions.set) && syncState2.isSyncEnabled.peek());
  if (saveLocal || saveRemote) {
    if (saveLocal && !syncState2.isPersistLoaded.peek()) {
      console.error(
        "[legend-state] WARNING: An observable was changed before being loaded from persist",
        persist
      );
      return void 0;
    }
    const changesLocal = [];
    const changesPaths = /* @__PURE__ */ new Set();
    let promisesTransform = [];
    for (let i = changes.length - 1; i >= 0; i--) {
      const { path } = changes[i];
      let found = false;
      if (changesPaths.size > 0) {
        for (let u = 0; u < path.length; u++) {
          if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join("/"))) {
            found = true;
            break;
          }
        }
      }
      if (!found) {
        const pathStr = path.join("/");
        changesPaths.add(pathStr);
        const { prevAtPath, valueAtPath, pathTypes } = changes[i];
        if (saveLocal) {
          const promiseTransformLocal = transformSaveData(
            valueAtPath,
            path,
            pathTypes,
            configLocal
          );
          promisesTransform.push(
            doInOrder(promiseTransformLocal, ({ value: valueTransformed, path: pathTransformed }) => {
              changesLocal.push({
                path: pathTransformed,
                pathTypes,
                prevAtPath,
                valueAtPath: valueTransformed,
                pathStr: path === pathTransformed ? pathStr : pathTransformed.join("/")
              });
            })
          );
        }
      }
    }
    promisesTransform = promisesTransform.filter(Boolean);
    if (promisesTransform.length > 0) {
      await Promise.all(promisesTransform);
    }
    return { queuedChange, changesLocal, saveRemote };
  }
}
async function prepChangeRemote(queuedChange) {
  const {
    syncState: syncState2,
    changes,
    localState,
    syncOptions,
    inRemoteChange,
    isApplyingPending
  } = queuedChange;
  const persist = syncOptions.persist;
  const { config: configLocal } = parseLocalConfig(persist);
  const saveLocal = persist && !configLocal.readonly && !isApplyingPending && syncState2.isPersistEnabled.peek();
  const saveRemote = !inRemoteChange && (syncOptions == null ? void 0 : syncOptions.set) && syncState2.isSyncEnabled.peek();
  if (saveLocal || saveRemote) {
    if (saveLocal && !syncState2.isPersistLoaded.peek()) {
      console.error(
        "[legend-state] WARNING: An observable was changed before being loaded from persist",
        persist
      );
      return void 0;
    }
    const changesRemote = [];
    const changesPaths = /* @__PURE__ */ new Set();
    let promisesTransform = [];
    for (let i = changes.length - 1; i >= 0; i--) {
      const { path } = changes[i];
      let found = false;
      if (changesPaths.size > 0) {
        for (let u = 0; u < path.length; u++) {
          if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join("/"))) {
            found = true;
            break;
          }
        }
      }
      if (!found) {
        const pathStr = path.join("/");
        changesPaths.add(pathStr);
        const { prevAtPath, valueAtPath, pathTypes } = changes[i];
        if (saveRemote) {
          const promiseTransformRemote = transformSaveData(
            valueAtPath,
            path,
            pathTypes,
            syncOptions || {}
          );
          promisesTransform.push(
            doInOrder(promiseTransformRemote, ({ value: valueTransformed, path: pathTransformed }) => {
              var _a;
              if (!localState.pendingChanges) {
                localState.pendingChanges = {};
              }
              let found2 = false;
              for (let i2 = 0; !found2 && i2 < pathTransformed.length - 1; i2++) {
                const pathParent = pathTransformed.slice(0, i2 + 1).join("/");
                if ((_a = localState.pendingChanges[pathParent]) == null ? void 0 : _a.v) {
                  found2 = true;
                  const pathChild = pathTransformed.slice(i2 + 1);
                  const pathTypesChild = pathTypes.slice(i2 + 1);
                  setAtPath(
                    localState.pendingChanges[pathParent].v,
                    pathChild,
                    pathTypesChild,
                    valueAtPath
                  );
                }
              }
              if (!found2) {
                for (const key in localState.pendingChanges) {
                  if (key !== pathStr && key.startsWith(pathStr)) {
                    delete localState.pendingChanges[key];
                  }
                }
                if (!localState.pendingChanges[pathStr]) {
                  localState.pendingChanges[pathStr] = { p: prevAtPath != null ? prevAtPath : null, t: pathTypes };
                }
                localState.pendingChanges[pathStr].v = valueAtPath;
              }
              changesRemote.push({
                path: pathTransformed,
                pathTypes,
                prevAtPath,
                valueAtPath: valueTransformed,
                pathStr
              });
            })
          );
        }
      }
    }
    promisesTransform = promisesTransform.filter(Boolean);
    if (promisesTransform.length > 0) {
      await Promise.all(promisesTransform);
    }
    return { queuedChange, changesRemote };
  }
}
async function doChangeLocal(changeInfo) {
  if (!changeInfo)
    return;
  const { queuedChange, changesLocal, saveRemote } = changeInfo;
  const { value$: obs, syncState: syncState2, localState, syncOptions } = queuedChange;
  const { pluginPersist } = localState;
  const persist = syncOptions.persist;
  const saveLocal = !!(persist == null ? void 0 : persist.name);
  if (saveLocal) {
    const { table, config: configLocal } = parseLocalConfig(persist);
    const shouldSaveMetadata = persist == null ? void 0 : persist.retrySync;
    if (saveRemote && shouldSaveMetadata) {
      await updateMetadataImmediate(obs, localState, syncState2, syncOptions, {
        pending: localState.pendingChanges
      });
    }
    if (changesLocal.length > 0) {
      let promiseSet = pluginPersist.set(table, changesLocal, configLocal);
      if (promiseSet) {
        promiseSet = promiseSet.then(() => {
          promisesLocalSaves.delete(promiseSet);
        });
        promisesLocalSaves.add(promiseSet);
        await promiseSet;
      }
    }
  }
}
async function doChangeRemote(changeInfo) {
  var _a, _b;
  if (!changeInfo)
    return;
  const { queuedChange, changesRemote } = changeInfo;
  const { value$: obs$, syncState: syncState2, localState, syncOptions } = queuedChange;
  const { pluginPersist } = localState;
  const node = getNode(obs$);
  const state$ = node.state;
  const persist = syncOptions.persist;
  const { table, config: configLocal } = parseLocalConfig(persist);
  const { onBeforeSet, waitForSet: waitForSetParam, onAfterSet } = syncOptions || {};
  const shouldSaveMetadata = persist == null ? void 0 : persist.retrySync;
  const saveLocal = !!(persist == null ? void 0 : persist.name);
  if (changesRemote.length > 0) {
    if (!syncState2.isLoaded.peek()) {
      await when(syncState2.isLoaded);
      const pending = localState.pendingChanges;
      if (pending) {
        changesRemote.forEach((change) => {
          const key = change.pathStr;
          const pendingAtPath = pending[key];
          if (!isNullOrUndefined(pendingAtPath)) {
            const { p } = pendingAtPath;
            change.prevAtPath = p;
          }
        });
      }
    }
    if (waitForSetParam) {
      await waitForSet(waitForSetParam, changesRemote, obs$.peek());
    }
    let value = clone2(obs$.peek());
    const transformSave = (_a = syncOptions == null ? void 0 : syncOptions.transform) == null ? void 0 : _a.save;
    if (transformSave) {
      value = transformSave(value);
    }
    state$.numPendingSets.set((v) => (v || 0) + 1);
    state$.isSetting.set(true);
    const beforeSetParams = {
      cancel: false
    };
    onBeforeSet == null ? void 0 : onBeforeSet(beforeSetParams);
    if (!beforeSetParams.cancel) {
      let updateResult = void 0;
      let lastErrorHandled;
      const onSetError = (error, params, noThrow) => {
        var _a2;
        if (lastErrorHandled !== error) {
          if (!params) {
            params = {
              setParams,
              source: "set",
              type: "set",
              input: value,
              retry: setParams,
              revert: createRevertChanges(setParams.value$, setParams.changes)
            };
          }
          state$.error.set(error);
          (_a2 = syncOptions.onError) == null ? void 0 : _a2.call(syncOptions, error, params);
          lastErrorHandled = error;
          if (!noThrow) {
            throw error;
          }
        }
      };
      const setParams = {
        node,
        value$: obs$,
        changes: changesRemote,
        value,
        onError: onSetError,
        update: (params) => {
          if (updateResult) {
            const { value: value2, mode, changes } = params;
            updateResult = {
              value: deepMerge(updateResult.value, value2),
              mode,
              changes: changes ? [...updateResult.changes || [], ...changes] : updateResult.changes
            };
          } else {
            updateResult = params;
          }
        },
        refresh: syncState2.sync,
        retryNum: 0,
        cancelRetry: false
      };
      const savedPromise = runWithRetry(setParams, syncOptions.retry, node, async () => {
        return syncOptions.set(setParams);
      });
      let didError = false;
      if (isPromise$1(savedPromise)) {
        await savedPromise.catch((error) => {
          didError = true;
          if (!syncOptions.retry) {
            onSetError(error, void 0, true);
          }
        });
      }
      if (!didError || (updateResult == null ? void 0 : updateResult.changes)) {
        const { value: updateValue, changes: updateChanges = changesRemote } = updateResult || {};
        const pathStrs = Array.from(
          new Set(updateChanges.map((change) => change.pathStr))
        );
        if (pathStrs.length > 0) {
          let transformedChanges = void 0;
          const metadata = {};
          if (saveLocal) {
            const pendingMetadata = (_b = pluginPersist.getMetadata(table, configLocal)) == null ? void 0 : _b.pending;
            const pending = localState.pendingChanges;
            for (let i = 0; i < pathStrs.length; i++) {
              const pathStr = pathStrs[i];
              if (pendingMetadata == null ? void 0 : pendingMetadata[pathStr]) {
                delete pendingMetadata[pathStr];
                metadata.pending = pendingMetadata;
              }
              if (pending == null ? void 0 : pending[pathStr]) {
                delete pending[pathStr];
              }
            }
          }
          if (updateValue && !isEmpty(updateValue)) {
            transformedChanges = transformLoadData(updateValue, syncOptions, false, "set");
          }
          if (transformedChanges !== void 0) {
            if (isPromise$1(transformedChanges)) {
              transformedChanges = await transformedChanges;
            }
            onChangeRemote2(() => mergeIntoObservable(obs$, transformedChanges));
          }
          if (saveLocal) {
            if (shouldSaveMetadata && !isEmpty(metadata)) {
              updateMetadata(obs$, localState, syncState2, syncOptions, metadata);
            }
          }
        }
        state$.numPendingSets.set((v) => v - 1);
        state$.isSetting.set(state$.numPendingSets.peek() > 0);
        onAfterSet == null ? void 0 : onAfterSet();
      }
    }
  }
}
function onObsChange(value$, syncState2, localState, syncOptions, { changes, isFromPersist, isFromSync, getPrevious }) {
  if (!isFromPersist) {
    const inRemoteChange = isFromSync;
    const isApplyingPending = localState.isApplyingPending;
    _queuedChanges.push({
      value$,
      syncState: syncState2,
      localState,
      syncOptions,
      changes,
      inRemoteChange,
      isApplyingPending,
      getPrevious
    });
    if (_queuedChanges.length === 1) {
      queueMicrotask(processQueuedChanges);
    }
  }
}
async function loadLocal(value$, syncOptions, syncState$, localState) {
  var _a, _b, _c;
  const { persist } = syncOptions;
  const node = getNode(value$);
  const nodeValue = getNodeValue(getNode(node.state));
  const syncStateValue = syncState$.peek();
  const prevResetPersistence = nodeValue.resetPersistence;
  if (persist == null ? void 0 : persist.name) {
    const PersistPlugin = persist.plugin || ((_a = observableSyncConfiguration.persist) == null ? void 0 : _a.plugin);
    const { table, config } = parseLocalConfig(persist);
    syncStateValue.numPendingLocalLoads = (syncStateValue.numPendingLocalLoads || 0) + 1;
    if (!PersistPlugin) {
      throw new Error("Local persist is not configured");
    }
    if (!mapSyncPlugins.has(PersistPlugin)) {
      const persistPlugin2 = isFunction(PersistPlugin) ? new PersistPlugin() : PersistPlugin;
      const mapValue = { plugin: persistPlugin2, initialized: observable(false) };
      mapSyncPlugins.set(PersistPlugin, mapValue);
      if (persistPlugin2.initialize) {
        const initializePromise = (_c = persistPlugin2.initialize) == null ? void 0 : _c.call(persistPlugin2, ((_b = observableSyncConfiguration) == null ? void 0 : _b.persist) || {});
        if (isPromise$1(initializePromise)) {
          await initializePromise;
        }
      }
      mapValue.initialized.set(true);
    }
    const { plugin, initialized: initialized$ } = mapSyncPlugins.get(PersistPlugin);
    const persistPlugin = plugin;
    localState.pluginPersist = persistPlugin;
    if (!initialized$.peek()) {
      await when(initialized$);
    }
    if (persistPlugin.loadTable) {
      try {
        const promise = persistPlugin.loadTable(table, config);
        if (promise) {
          await promise;
        }
      } catch (err) {
        if (process.env.NODE_ENV === "development") {
          console.error(
            "[legend-state] Error loading local cache. This would be a crashing error in production.",
            err
          );
        } else {
          throw err;
        }
      }
    }
    const prevValue = getNodeValue(node);
    let value = persistPlugin.getTable(table, prevValue, config);
    const metadata = persistPlugin.getMetadata(table, config);
    if (metadata) {
      metadatas.set(value$, metadata);
      localState.pendingChanges = metadata.pending;
      syncState$.assign({
        lastSync: metadata.lastSync
      });
    }
    if (value !== void 0) {
      const { transform } = config;
      value = transformLoadData(value, { transform }, true, "get");
      if (isPromise$1(value)) {
        value = await value;
      }
      node.root.isLoadingLocal = true;
      internal.globalState.isLoadingLocal = true;
      if (value === null && (!prevValue || prevValue[symbolLinked])) {
        value$.set(value);
      } else {
        mergeIntoObservable(value$, value);
      }
      node.root.isLoadingLocal = false;
      internal.globalState.isLoadingLocal = false;
    }
    syncStateValue.numPendingLocalLoads--;
    nodeValue.resetPersistence = () => Promise.all(
      [
        prevResetPersistence,
        persistPlugin.deleteTable(table, config),
        persistPlugin.deleteMetadata(table, config)
      ].filter(Boolean)
    );
  } else {
    if ((process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") && persist && persist.plugin && !Object.hasOwn(persist, "name")) {
      console.warn(
        "[legend-state] Trying to syncObservable without `name` defined. Please include a `name` property in the `persist` configuration."
      );
    }
    nodeValue.resetPersistence = () => prevResetPersistence == null ? void 0 : prevResetPersistence();
  }
  nodeValue.clearPersist = nodeValue.resetPersistence;
  syncState$.isPersistLoaded.set(!(syncStateValue.numPendingLocalLoads > 0));
}
function syncObservable(obs$, syncOptionsOrSynced) {
  let syncOptions = syncOptionsOrSynced;
  if (isFunction(syncOptions)) {
    syncOptions = syncOptions()[symbolLinked];
  }
  const node = getNode(obs$);
  if ((process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") && (!obs$ || !node)) {
    throw new Error("[legend-state] syncObservable called with undefined observable");
  }
  syncOptions = deepMerge(
    {
      syncMode: "auto"
    },
    observableSyncConfiguration,
    removeNullUndefined(syncOptions || {})
  );
  const localState = {};
  let sync;
  const syncState$ = syncState(obs$);
  const syncStateValue = getNodeValue(getNode(syncState$));
  allSyncStates.set(syncState$, node);
  syncStateValue.getPendingChanges = () => localState.pendingChanges;
  let lastErrorHandled;
  const onGetError = (error, params, noThrow) => {
    var _a;
    if (lastErrorHandled !== error) {
      if (!params) {
        params = {
          source: "get",
          type: "get",
          retry: params
        };
      }
      syncState$.error.set(error);
      (_a = syncOptions.onError) == null ? void 0 : _a.call(syncOptions, error, params);
      lastErrorHandled = error;
      if (!noThrow) {
        throw error;
      }
    }
  };
  loadLocal(obs$, syncOptions, syncState$, localState);
  let isWaitingForLoad = !!syncOptions.get;
  if (isWaitingForLoad) {
    syncStateValue.numPendingRemoteLoads = (syncStateValue.numPendingRemoteLoads || 0) + 1;
  }
  syncState$.isLoaded.set(!syncState$.numPendingRemoteLoads.peek());
  let isSynced = false;
  let isSubscribed = false;
  let isApplyingPendingAfterSync = false;
  let unsubscribe = void 0;
  const applyPending = (pending) => {
    if (pending && !isEmpty(pending)) {
      const keys = Object.keys(pending);
      const value = getNodeValue(node);
      const changes = [];
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        const path = key.split("/").filter((p2) => p2 !== "");
        const { p, t, v } = pending[key];
        const valueAtPath = getValueAtPath(value, path);
        if (isApplyingPendingAfterSync || !deepEqual(valueAtPath, v)) {
          changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
        }
      }
      if (changes.length > 0) {
        localState.isApplyingPending = true;
        onObsChange(obs$, syncState$, localState, syncOptions, {
          isFromPersist: false,
          isFromSync: false,
          getPrevious: createPreviousHandler(value, changes),
          changes
        });
        localState.isApplyingPending = false;
      }
    }
  };
  const { get, subscribe } = syncOptions;
  if (get || subscribe) {
    sync = async () => {
      var _a;
      if (isSynced && (!getNodeValue(getNode(syncState$)).isSyncEnabled || shouldIgnoreUnobserved(node, sync))) {
        if (unsubscribe) {
          isSubscribed = false;
          unsubscribe();
          unsubscribe = void 0;
        }
        return;
      }
      const lastSync = (_a = metadatas.get(obs$)) == null ? void 0 : _a.lastSync;
      const pending = localState.pendingChanges;
      if (get || subscribe) {
        const { waitFor } = syncOptions;
        const runGet = () => {
          var _a2;
          const onChange = async ({ value, mode, lastSync: lastSync2 }) => {
            mode = mode || syncOptions.mode || "set";
            if (value !== void 0) {
              value = transformLoadData(value, syncOptions, true, "get");
              if (isPromise$1(value)) {
                value = await value;
              }
              const pending2 = localState.pendingChanges;
              const currentValue = obs$.peek();
              if (pending2) {
                let didChangeMetadata = false;
                Object.keys(pending2).forEach((key) => {
                  const p = key.split("/").filter((k) => k !== "");
                  const { v, t } = pending2[key];
                  if (t.length === 0 || !value) {
                    const oldValue = clone2(value);
                    pending2[key].p = key ? oldValue[key] : oldValue;
                    if (isObject(value) && isObject(v)) {
                      Object.assign(value, key ? { [key]: v } : v);
                    } else if (!key) {
                      value = v;
                    }
                  } else if (value[p[0]] !== void 0) {
                    const curValue = getValueAtPath(currentValue, p);
                    const newValue = getValueAtPath(value, p);
                    if (JSON.stringify(curValue) === JSON.stringify(newValue)) {
                      delete pending2[key];
                      didChangeMetadata = true;
                    } else {
                      const oldValue = clone2(value);
                      pending2[key].p = getValueAtPath(oldValue, p);
                      didChangeMetadata = true;
                      value = setAtPath(
                        value,
                        p,
                        t,
                        v,
                        "merge",
                        obs$.peek(),
                        (path, value2) => {
                          delete pending2[key];
                          pending2[path.join("/")] = {
                            p: null,
                            v: value2,
                            t: t.slice(0, path.length)
                          };
                        }
                      );
                    }
                  }
                });
                if (didChangeMetadata && syncOptions.persist) {
                  updateMetadataImmediate(obs$, localState, syncState$, syncOptions, {
                    pending: pending2
                  });
                }
              }
              onChangeRemote2(() => {
                if (isPlainObject(value)) {
                  value = ObservableHint.plain(value);
                }
                if (mode === "assign") {
                  obs$.assign(value);
                } else if (mode === "append") {
                  if ((process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") && !isArray(value)) {
                    console.error("[legend-state] mode:append expects the value to be an array");
                  }
                  obs$.push(...value);
                } else if (mode === "prepend") {
                  if ((process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") && !isArray(value)) {
                    console.error("[legend-state] mode:prepend expects the value to be an array");
                  }
                  obs$.splice(0, 0, ...value);
                } else if (mode === "merge") {
                  mergeIntoObservable(obs$, value);
                } else {
                  obs$.set(value);
                }
              });
            }
            if (lastSync2 && syncOptions.persist) {
              updateMetadata(obs$, localState, syncState$, syncOptions, {
                lastSync: lastSync2
              });
            }
          };
          if (node.activationState) {
            node.activationState.onChange = onChange;
          }
          if (!isSubscribed && syncOptions.subscribe) {
            const subscribe2 = syncOptions.subscribe;
            isSubscribed = true;
            const doSubscribe = () => {
              const subscribeParams = {
                node,
                value$: obs$,
                lastSync,
                update: (params) => {
                  when(
                    () => !get || syncState$.isLoaded.get(),
                    () => {
                      when(waitFor || true, () => {
                        params.mode || (params.mode = syncOptions.mode || "merge");
                        onChange(params);
                        if (!syncState$.isLoaded.peek()) {
                          syncState$.assign({
                            isLoaded: syncStateValue.numPendingRemoteLoads < 1,
                            error: void 0,
                            isGetting: syncStateValue.numPendingGets > 0
                          });
                        }
                      });
                    }
                  );
                },
                refresh: () => when(syncState$.isLoaded, sync),
                onError: (error) => onGetError(error, {
                  source: "subscribe",
                  subscribeParams,
                  type: "get",
                  retry: {}
                })
              };
              unsubscribe = subscribe2(subscribeParams);
            };
            if (waitFor) {
              whenReady(waitFor, doSubscribe);
            } else {
              doSubscribe();
            }
          }
          const existingValue = getNodeValue(node);
          if (get) {
            const getParams = {
              node,
              value$: obs$,
              value: isFunction(existingValue) || (existingValue == null ? void 0 : existingValue[symbolLinked]) ? void 0 : existingValue,
              mode: syncOptions.mode,
              refresh: sync,
              options: syncOptions,
              lastSync,
              updateLastSync: (lastSync2) => getParams.lastSync = lastSync2,
              onError: onGetError,
              retryNum: 0,
              cancelRetry: false
            };
            let modeBeforeReset = void 0;
            const beforeGetParams = {
              value: getParams.value,
              lastSync,
              pendingChanges: pending && !isEmpty(pending) ? pending : void 0,
              clearPendingChanges: async () => {
                localState.pendingChanges = {};
                await updateMetadataImmediate(obs$, localState, syncState$, syncOptions, {
                  pending: localState.pendingChanges
                });
              },
              resetCache: () => {
                var _a3;
                modeBeforeReset = getParams.mode;
                getParams.mode = "set";
                return (_a3 = syncStateValue.resetPersistence) == null ? void 0 : _a3.call(syncStateValue);
              },
              cancel: false
            };
            (_a2 = syncOptions.onBeforeGet) == null ? void 0 : _a2.call(syncOptions, beforeGetParams);
            if (!beforeGetParams.cancel) {
              syncState$.assign({
                numPendingGets: (syncStateValue.numPendingGets || 0) + 1,
                isGetting: true
              });
              const got = runWithRetry(getParams, syncOptions.retry, node, (retryEvent) => {
                const params = getParams;
                params.cancelRetry = retryEvent.cancelRetry;
                params.retryNum = retryEvent.retryNum;
                return get(params);
              });
              const numGets = node.numGets = (node.numGets || 0) + 1;
              const handle = (value) => {
                syncState$.numPendingGets.set((v) => v - 1);
                if (isWaitingForLoad) {
                  isWaitingForLoad = false;
                  syncStateValue.numPendingRemoteLoads--;
                }
                if (numGets >= (node.getNumResolved || 0)) {
                  node.getNumResolved = node.numGets;
                  onChange({
                    value,
                    lastSync: getParams.lastSync,
                    mode: getParams.mode
                  });
                }
                if (modeBeforeReset) {
                  getParams.mode = modeBeforeReset;
                  modeBeforeReset = void 0;
                }
                syncState$.assign({
                  isLoaded: syncStateValue.numPendingRemoteLoads < 1,
                  error: void 0,
                  isGetting: syncStateValue.numPendingGets > 0
                });
              };
              if (isPromise$1(got)) {
                got.then(handle).catch((error) => {
                  onGetError(
                    error,
                    { getParams, source: "get", type: "get", retry: getParams },
                    true
                  );
                });
              } else {
                handle(got);
              }
            }
          }
        };
        if (waitFor) {
          whenReady(waitFor, () => trackSelector(runGet, sync));
        } else {
          trackSelector(runGet, sync);
        }
      } else {
        syncState$.assign({
          isLoaded: true,
          error: void 0
        });
      }
      if (!isSynced) {
        isSynced = true;
        isApplyingPendingAfterSync = true;
        applyPending(pending);
        isApplyingPendingAfterSync = false;
      }
    };
    syncStateValue.sync = sync;
  } else {
    if (!isSynced) {
      isApplyingPendingAfterSync = true;
      applyPending(localState.pendingChanges);
      isApplyingPendingAfterSync = false;
    }
  }
  syncStateValue.reset = async () => {
    const wasPersistEnabled = syncStateValue.isPersistEnabled;
    const wasSyncEnabled = syncStateValue.isSyncEnabled;
    const metadata = metadatas.get(obs$);
    if (metadata) {
      Object.assign(metadata, { lastSync: void 0, pending: void 0 });
    }
    Object.assign(syncStateValue, {
      isPersistEnabled: false,
      isSyncEnabled: false,
      lastSync: void 0,
      numPendingGets: 0,
      isLoaded: false,
      isGetting: false,
      isSetting: false,
      numPendingSets: 0,
      syncCount: 0
    });
    isSynced = false;
    isSubscribed = false;
    unsubscribe == null ? void 0 : unsubscribe();
    unsubscribe = void 0;
    const promise = syncStateValue.resetPersistence();
    onChangeRemote2(() => {
      var _a;
      obs$.set((_a = syncOptions.initial) != null ? _a : void 0);
    });
    syncState$.isLoaded.set(false);
    syncStateValue.isPersistEnabled = wasPersistEnabled;
    syncStateValue.isSyncEnabled = wasSyncEnabled;
    node.dirtyFn = sync;
    await promise;
  };
  const onAllPersistLoaded = () => {
    var _a, _b;
    let parentNode = node;
    while (parentNode) {
      if (((_b = (_a = parentNode.state) == null ? void 0 : _a.isPersistLoaded) == null ? void 0 : _b.get()) === false) {
        return false;
      }
      parentNode = parentNode.parent;
    }
    return true;
  };
  when(onAllPersistLoaded, function() {
    if ((syncOptions.get || syncOptions.subscribe) && syncOptions.syncMode === "auto") {
      sync();
    }
    if ((syncOptions == null ? void 0 : syncOptions.set) || (syncOptions == null ? void 0 : syncOptions.persist)) {
      obs$.onChange(
        onObsChange.bind(this, obs$, syncState$, localState, syncOptions)
      );
    }
  });
  return syncState$;
}
export {
  createRevertChanges,
  deepEqual,
  mapSyncPlugins,
  onChangeRemote2 as onChangeRemote,
  removeNullUndefined,
  syncObservable
};
//# sourceMappingURL=sync.js.map
