@legendapp/state
Version:
710 lines (703 loc) • 34.8 kB
JavaScript
import { symbolDelete, isString, isArray, isObject, internal as internal$1, constructObjectWithPath, deconstructObjectWithPath, observable, when, batch, isObservable, isFunction, getNode, isPromise, getNodeValue, mergeIntoObservable, isEmpty, setInObservableAtPath, setAtPath } from '@legendapp/state';
const observablePersistConfiguration = {};
function configureObservablePersistence(options) {
Object.assign(observablePersistConfiguration, options);
}
const { initializePathType } = internal$1;
let validateMap;
function transformPath(path, pathTypes, map) {
const data = {};
let d = data;
for (let i = 0; i < path.length; i++) {
d = d[path[i]] = i === path.length - 1 ? null : initializePathType(pathTypes[i]);
}
let value = transformObject(data, map);
const pathOut = [];
for (let i = 0; i < path.length; i++) {
const key = Object.keys(value)[0];
pathOut.push(key);
value = value[key];
}
return pathOut;
}
function transformObject(dataIn, map) {
if (process.env.NODE_ENV === 'development') {
validateMap(map);
}
let ret = dataIn;
if (dataIn) {
if (dataIn === symbolDelete)
return dataIn;
if (isString(dataIn)) {
return map[dataIn];
}
ret = {};
const dict = Object.keys(map).length === 1 && map['_dict'];
for (const key in dataIn) {
let v = dataIn[key];
if (dict) {
ret[key] = transformObject(v, dict);
}
else {
const mapped = map[key];
if (mapped === undefined) {
// Don't transform dateModified if user doesn't want it
if (key !== '@') {
ret[key] = v;
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
console.error('A fatal field transformation error has occurred', key, dataIn, map);
}
}
}
else if (mapped !== null) {
if (v !== undefined && v !== null) {
if (map[key + '_val']) {
const mapChild = map[key + '_val'];
if (isArray(v)) {
v = v.map((vChild) => mapChild[vChild]);
}
else {
v = mapChild[v];
}
}
else if (map[key + '_arr'] && isArray(v)) {
const mapChild = map[key + '_arr'];
v = v.map((vChild) => transformObject(vChild, mapChild));
}
else if (isObject(v)) {
if (map[key + '_obj']) {
v = transformObject(v, map[key + '_obj']);
}
else if (map[key + '_dict']) {
const mapChild = map[key + '_dict'];
const out = {};
for (const keyChild in v) {
out[keyChild] = transformObject(v[keyChild], mapChild);
}
v = out;
}
}
}
ret[mapped] = v;
}
}
}
}
return ret;
}
function transformObjectWithPath(obj, path, pathTypes, fieldTransforms) {
const constructed = constructObjectWithPath(path, pathTypes, obj);
const transformed = transformObject(constructed, fieldTransforms);
const transformedPath = transformPath(path, pathTypes, fieldTransforms);
return { path: transformedPath, obj: deconstructObjectWithPath(transformedPath, pathTypes, transformed) };
}
const invertedMaps = new WeakMap();
function invertFieldMap(obj) {
const existing = invertedMaps.get(obj);
if (existing)
return existing;
const target = {};
for (const key in obj) {
const val = obj[key];
if (key === '_dict') {
target[key] = invertFieldMap(val);
}
else if (key.endsWith('_obj') || key.endsWith('_dict') || key.endsWith('_arr') || key.endsWith('_val')) {
const keyMapped = obj[key.replace(/_obj|_dict|_arr|_val$/, '')];
const suffix = key.match(/_obj|_dict|_arr|_val$/)[0];
target[keyMapped + suffix] = invertFieldMap(val);
}
else if (typeof val === 'string') {
target[val] = key;
}
}
invertedMaps.set(obj, target);
return target;
}
if (process.env.NODE_ENV === 'development') {
validateMap = function (record) {
const values = Object.values(record).filter((value) => {
if (isObject(value)) {
validateMap(value);
}
else {
return isString(value);
}
});
const uniques = Array.from(new Set(values));
if (values.length !== uniques.length) {
console.error('Field transform map has duplicate values', record, values.length, uniques.length);
}
return record;
};
}
function observablePersistRemoteFunctionsAdapter({ get, set, }) {
const ret = {};
if (get) {
ret.get = (async (params) => {
const value = (await get(params));
params.onChange({ value, dateModified: Date.now() });
params.onGet();
});
}
if (set) {
ret.set = set;
}
return ret;
}
const mapPersistences = new WeakMap();
const persistState = observable({ inRemoteSync: false });
const metadatas = new WeakMap();
const promisesLocalSaves = new Set();
function parseLocalConfig(config) {
return config
? isString(config)
? { table: config, config: { name: config } }
: { table: config.name, config }
: {};
}
function doInOrder(arg1, arg2) {
return isPromise(arg1) ? arg1.then(arg2) : arg2(arg1);
}
function transformOutData(value, path, pathTypes, { transform, fieldTransforms }) {
if (fieldTransforms || (transform === null || transform === void 0 ? void 0 : transform.out)) {
const transformFn = () => {
if (fieldTransforms) {
const { obj, path: pathTransformed } = transformObjectWithPath(value, path, pathTypes, fieldTransforms);
value = obj;
path = pathTransformed;
}
return { value, path };
};
if (transform === null || transform === void 0 ? void 0 : transform.out) {
const constructed = constructObjectWithPath(path, pathTypes, value);
const saved = transform.out(constructed);
const deconstruct = (toDeconstruct) => {
value = deconstructObjectWithPath(path, pathTypes, toDeconstruct);
return transformFn();
};
return doInOrder(saved, deconstruct);
}
return transformFn();
}
return { value, path };
}
function transformLoadData(value, { transform, fieldTransforms }, doUserTransform) {
if (fieldTransforms) {
const inverted = invertFieldMap(fieldTransforms);
value = transformObject(value, inverted);
}
if (doUserTransform && (transform === null || transform === void 0 ? void 0 : transform.in)) {
value = transform.in(value);
}
return value;
}
async function updateMetadataImmediate(obs, localState, syncState, persistOptions, newMetadata) {
const saves = Array.from(promisesLocalSaves);
if (saves.length > 0) {
await Promise.all(saves);
}
const { persistenceLocal } = localState;
const local = persistOptions.local;
const { table, config } = parseLocalConfig(local);
// Save metadata
const oldMetadata = metadatas.get(obs);
const { modified, pending } = newMetadata;
const needsUpdate = pending || (modified && (!oldMetadata || modified !== oldMetadata.modified));
if (needsUpdate) {
const metadata = Object.assign({}, oldMetadata, newMetadata);
metadatas.set(obs, metadata);
if (persistenceLocal) {
await persistenceLocal.setMetadata(table, metadata, config);
}
if (modified) {
syncState.dateModified.set(modified);
}
}
}
function updateMetadata(obs, localState, syncState, persistOptions, newMetadata) {
if (localState.timeoutSaveMetadata) {
clearTimeout(localState.timeoutSaveMetadata);
}
localState.timeoutSaveMetadata = setTimeout(() => updateMetadataImmediate(obs, localState, syncState, persistOptions, newMetadata), 30);
}
let _queuedChanges = [];
async function processQueuedChanges() {
// Get a local copy of the queued changes and clear the global queue
const queuedChanges = _queuedChanges;
_queuedChanges = [];
// Note: Summary of the order of operations these functions:
// 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
// We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
// since some may take longer to transformSaveData than others.
const changes = await Promise.all(queuedChanges.map(prepChange));
// 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
// the current value again on next load, which isn't too bad.
// 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
// and attempted again.
// 4. Wait for remote load or error if allowed
// 5. Save to remote
// 6. On successful save, merge changes (if any) back into observable
// 7. Lastly, update metadata to clear pending and update dateModified. Doing this earlier could potentially cause
// sync inconsistences so it's very important that this is last.
changes.forEach(doChange);
}
async function prepChange(queuedChange) {
const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
const local = persistOptions.local;
const { persistenceRemote } = localState;
const { config: configLocal } = parseLocalConfig(local);
const configRemote = persistOptions.remote;
const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
const saveRemote = !inRemoteChange && persistenceRemote && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
if (saveLocal || saveRemote) {
if (saveLocal && !syncState.isLoadedLocal.peek()) {
console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
return;
}
const changesLocal = [];
const changesRemote = [];
const changesPaths = new Set();
let promisesTransform = [];
// Reverse order
for (let i = changes.length - 1; i >= 0; i--) {
const { path } = changes[i];
let found = false;
// Optimization to only save the latest update at each path. We might have multiple changes at the same path
// and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
// already processed. If a later change modifies a parent of an earlier change (which happens on delete()
// it should be ignored as it's superseded by the parent modification.
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 = transformOutData(valueAtPath, path, pathTypes, configLocal);
promisesTransform.push(doInOrder(promiseTransformLocal, ({ path: pathTransformed, value: valueTransformed }) => {
// If path includes undefined there was a null in fieldTransforms so don't need to save it
if (!pathTransformed.includes(undefined)) {
// Prepare the local change with the transformed path/value
changesLocal.push({
path: pathTransformed,
pathTypes,
prevAtPath,
valueAtPath: valueTransformed,
pathStr,
});
}
}));
}
if (saveRemote) {
const promiseTransformRemote = transformOutData(valueAtPath, path, pathTypes, configRemote || {});
promisesTransform.push(doInOrder(promiseTransformRemote, ({ path: pathTransformed, value: valueTransformed }) => {
var _a;
// If path includes undefined there was a null in fieldTransforms so don't need to save it
if (!pathTransformed.includes(undefined)) {
// Prepare pending changes
if (!localState.pendingChanges) {
localState.pendingChanges = {};
}
// First look for existing pending changes at a higher level than this change
// If they exist then merge this change into it
let found = false;
for (let i = 0; !found && i < path.length - 1; i++) {
const pathParent = path.slice(0, i + 1).join('/');
if ((_a = localState.pendingChanges[pathParent]) === null || _a === void 0 ? void 0 : _a.v) {
found = true;
const pathChild = path.slice(i + 1);
const pathTypesChild = pathTypes.slice(i + 1);
setAtPath(localState.pendingChanges[pathParent].v, pathChild, pathTypesChild, valueAtPath);
}
}
if (!found) {
// If an existing pending change is deeper than this change, just delete it
// in favor of this wider change
for (const key in localState.pendingChanges) {
if (key !== pathStr && key.startsWith(pathStr)) {
delete localState.pendingChanges[key];
}
}
// The "p" saved in pending should be the previous state before changes,
// so don't overwrite it if it already exists
if (!localState.pendingChanges[pathStr]) {
localState.pendingChanges[pathStr] = { p: prevAtPath !== null && prevAtPath !== void 0 ? prevAtPath : null, t: pathTypes };
}
// Pending value is the untransformed value because it gets loaded without transformment
// and forwarded through to onObsChange where it gets transformed before save
localState.pendingChanges[pathStr].v = valueAtPath;
}
// Prepare the remote change with the transformed path/value
changesRemote.push({
path: pathTransformed,
pathTypes,
prevAtPath,
valueAtPath: valueTransformed,
pathStr,
});
}
}));
}
}
}
// If there's any transform promises, wait for them before saving
promisesTransform = promisesTransform.filter(Boolean);
if (promisesTransform.length > 0) {
await Promise.all(promisesTransform);
}
return { queuedChange, changesLocal, changesRemote };
}
}
async function doChange(changeInfo) {
var _a, _b, _c, _d;
if (!changeInfo)
return;
const { queuedChange, changesLocal, changesRemote } = changeInfo;
const { obs, syncState, localState, persistOptions } = queuedChange;
const { persistenceLocal, persistenceRemote } = localState;
const local = persistOptions.local;
const { table, config: configLocal } = parseLocalConfig(local);
const configRemote = persistOptions.remote;
const shouldSaveMetadata = local && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.offlineBehavior) === 'retry';
if (changesRemote.length > 0 && shouldSaveMetadata) {
// First save pending changes before saving local or remote
await updateMetadataImmediate(obs, localState, syncState, persistOptions, {
pending: localState.pendingChanges,
});
}
if (changesLocal.length > 0) {
// Save the changes to local persistence before saving to remote. They are already marked as pending so
// if remote sync fails or the app is closed before remote sync, it will attempt to sync them on the next load.
let promiseSet = persistenceLocal.set(table, changesLocal, configLocal);
if (promiseSet) {
promiseSet = promiseSet.then(() => {
promisesLocalSaves.delete(promiseSet);
});
// Keep track of local save promises so that updateMetadata runs only after everything is saved
promisesLocalSaves.add(promiseSet);
// await the local save before proceeding to save remotely
await promiseSet;
}
}
if (changesRemote.length > 0) {
// Wait for remote to be ready before saving
await when(() => syncState.isLoaded.get() || ((configRemote === null || configRemote === void 0 ? void 0 : configRemote.allowSetIfError) && syncState.error.get()));
const value = obs.peek();
(_a = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onBeforeSet) === null || _a === void 0 ? void 0 : _a.call(configRemote);
const saved = await ((_b = persistenceRemote.set({
obs,
syncState: syncState,
options: persistOptions,
changes: changesRemote,
value,
})) === null || _b === void 0 ? void 0 : _b.catch((err) => { var _a; return (_a = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onSetError) === null || _a === void 0 ? void 0 : _a.call(configRemote, err); }));
// If this remote save changed anything then update persistence and metadata
// Because save happens after a timeout and they're batched together, some calls to save will
// return saved data and others won't, so those can be ignored.
if (saved) {
const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
const { changes, dateModified } = saved;
if (pathStrs.length > 0) {
if (local) {
const metadata = {};
const pending = (_c = persistenceLocal.getMetadata(table, configLocal)) === null || _c === void 0 ? void 0 : _c.pending;
let transformedChanges = [];
for (let i = 0; i < pathStrs.length; i++) {
const pathStr = pathStrs[i];
// Clear pending for this path
if (pending === null || pending === void 0 ? void 0 : pending[pathStr]) {
// Remove pending from local state
delete pending[pathStr];
metadata.pending = pending;
}
}
if (dateModified) {
metadata.modified = dateModified;
}
// Remote can optionally have data that needs to be merged back into the observable,
// for example Firebase may update dateModified with the server timestamp
if (changes && !isEmpty(changes)) {
transformedChanges.push(transformLoadData(changes, persistOptions.remote, false));
}
if (transformedChanges.length > 0) {
if (transformedChanges.some((change) => isPromise(change))) {
transformedChanges = await Promise.all(transformedChanges);
}
onChangeRemote(() => mergeIntoObservable(obs, ...transformedChanges));
}
if (shouldSaveMetadata && !isEmpty(metadata)) {
updateMetadata(obs, localState, syncState, persistOptions, metadata);
}
}
(_d = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onSet) === null || _d === void 0 ? void 0 : _d.call(configRemote);
}
}
}
}
function onObsChange(obs, syncState, localState, persistOptions, { changes }) {
if (!internal$1.globalState.isLoadingLocal) {
const inRemoteChange = internal$1.globalState.isLoadingRemote;
const isApplyingPending = localState.isApplyingPending;
// Queue changes in a microtask so that multiple changes within a frame get run together
_queuedChanges.push({
obs: obs,
syncState,
localState,
persistOptions,
changes,
inRemoteChange,
isApplyingPending: isApplyingPending,
});
if (_queuedChanges.length === 1) {
queueMicrotask(processQueuedChanges);
}
}
}
function onChangeRemote(cb) {
when(() => !persistState.inRemoteSync.get(), () => {
// Remote changes should only update local state
persistState.inRemoteSync.set(true);
internal$1.globalState.isLoadingRemote = true;
batch(cb, () => {
internal$1.globalState.isLoadingRemote = false;
persistState.inRemoteSync.set(false);
});
});
}
async function loadLocal(obs, persistOptions, syncState, localState) {
var _a;
const { local } = persistOptions;
const localPersistence = persistOptions.pluginLocal || observablePersistConfiguration.pluginLocal;
if (local) {
const { table, config } = parseLocalConfig(local);
const node = getNode(obs);
if (!localPersistence) {
throw new Error('Local persistence is not configured');
}
// Ensure there's only one instance of the persistence plugin
if (!mapPersistences.has(localPersistence)) {
const persistenceLocal = new localPersistence();
const mapValue = { persist: persistenceLocal, initialized: observable(false) };
mapPersistences.set(localPersistence, mapValue);
if (persistenceLocal.initialize) {
const initializePromise = (_a = persistenceLocal.initialize) === null || _a === void 0 ? void 0 : _a.call(persistenceLocal, observablePersistConfiguration.localOptions || {});
if (isPromise(initializePromise)) {
await initializePromise;
}
}
mapValue.initialized.set(true);
}
const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
localState.persistenceLocal = persistenceLocal;
if (!initialized.get()) {
await when(initialized);
}
// If persistence has an asynchronous load, wait for it
if (persistenceLocal.loadTable) {
const promise = persistenceLocal.loadTable(table, config);
if (promise) {
await promise;
}
}
// Get current value for init
const prevValue = getNodeValue(node);
// Get the value from state
let value = persistenceLocal.getTable(table, config, prevValue);
const metadata = persistenceLocal.getMetadata(table, config);
if (metadata) {
metadatas.set(obs, metadata);
localState.pendingChanges = metadata.pending;
syncState.dateModified.set(metadata.modified);
}
// Merge the data from local persistence into the default state
if (value !== null && value !== undefined) {
const { transform, fieldTransforms } = config;
value = transformLoadData(value, { transform, fieldTransforms }, true);
if (isPromise(value)) {
value = await value;
}
batch(() => {
// isLoadingLocal prevents saving remotely when two different persistences
// are set on the same observable
internal$1.globalState.isLoadingLocal = true;
// We want to merge the local data on top of any initial state the object is created with
mergeIntoObservable(obs, value);
}, () => {
internal$1.globalState.isLoadingLocal = false;
});
}
node.state.peek().clearLocal = () => Promise.all([
persistenceLocal.deleteTable(table, config),
persistenceLocal.deleteMetadata(table, config),
]);
}
syncState.isLoadedLocal.set(true);
}
function persistObservable(initialOrObservable, persistOptions) {
var _a;
const obs = (isObservable(initialOrObservable)
? initialOrObservable
: observable(isFunction(initialOrObservable) ? initialOrObservable() : initialOrObservable));
const node = getNode(obs);
if (process.env.NODE_ENV === 'development' && ((_a = obs === null || obs === void 0 ? void 0 : obs.peek()) === null || _a === void 0 ? void 0 : _a._state)) {
console.warn('[legend-state] WARNING: persistObservable creates a property named "_state" but your observable already has "state" in it');
}
// Merge remote persist options with clobal options
if (persistOptions.remote) {
persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, persistOptions.remote);
}
let { remote } = persistOptions;
const { local } = persistOptions;
const remotePersistence = persistOptions.pluginRemote || (observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.pluginRemote);
const localState = {};
const syncState = (node.state = observable({
isLoadedLocal: false,
isLoaded: false,
isEnabledLocal: true,
isEnabledRemote: true,
clearLocal: undefined,
sync: () => Promise.resolve(),
getPendingChanges: () => localState.pendingChanges,
}));
loadLocal(obs, persistOptions, syncState, localState);
if (remote || remotePersistence) {
if (!remotePersistence) {
throw new Error('Remote persistence is not configured');
}
if (!remote) {
remote = {};
}
if (isObject(remotePersistence)) {
localState.persistenceRemote = observablePersistRemoteFunctionsAdapter(remotePersistence);
}
else {
// Ensure there's only one instance of the persistence plugin
if (!mapPersistences.has(remotePersistence)) {
mapPersistences.set(remotePersistence, {
persist: new remotePersistence(),
});
}
localState.persistenceRemote = mapPersistences.get(remotePersistence)
.persist;
}
let isSynced = false;
const sync = async () => {
var _a, _b;
if (!isSynced) {
isSynced = true;
const dateModified = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.modified;
const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
if (get) {
get({
state: syncState,
obs,
options: persistOptions,
dateModified,
onGet: () => {
syncState.isLoaded.set(true);
},
onChange: async ({ value, path = [], pathTypes = [], mode = 'set', dateModified }) => {
// Note: value is the constructed value, path is used for setInObservableAtPath
// to start the set into the observable from the path
if (value !== undefined) {
value = transformLoadData(value, remote, true);
if (isPromise(value)) {
value = await value;
}
const invertedMap = remote.fieldTransforms && invertFieldMap(remote.fieldTransforms);
if (path.length && invertedMap) {
path = transformPath(path, pathTypes, invertedMap);
}
if (mode === 'dateModified') {
if (dateModified && !isEmpty(value)) {
onChangeRemote(() => {
setInObservableAtPath(obs, path, pathTypes, value, 'assign');
});
}
}
else {
const pending = localState.pendingChanges;
if (pending) {
Object.keys(pending).forEach((key) => {
const p = key.split('/').filter((p) => p !== '');
const { v, t } = pending[key];
if (value[p[0]] !== undefined) {
value = setAtPath(value, p, t, v, obs.peek(), (path, value) => {
delete pending[key];
pending[path.join('/')] = {
p: null,
v: value,
t: t.slice(0, path.length),
};
});
}
});
}
onChangeRemote(() => {
setInObservableAtPath(obs, path, pathTypes, value, mode);
});
}
}
if (dateModified && local) {
updateMetadata(obs, localState, syncState, persistOptions, {
modified: dateModified,
});
}
},
});
}
else {
syncState.isLoaded.set(true);
}
// Wait for remote to be ready before saving pending
await when(() => syncState.isLoaded.get() || (remote.allowSetIfError && syncState.error.get()));
const pending = localState.pendingChanges;
if (pending && !isEmpty(pending)) {
localState.isApplyingPending = true;
const keys = Object.keys(pending);
// Bundle up all the changes from pending
const changes = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const path = key.split('/').filter((p) => p !== '');
const { p, v, t } = pending[key];
changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
}
// Send the changes into onObsChange so that they get persisted remotely
onObsChange(obs, syncState, localState, persistOptions, {
value: obs.peek(),
// TODO getPrevious if any remote persistence layers need it
getPrevious: () => undefined,
changes,
});
localState.isApplyingPending = false;
}
}
};
if (remote.manual) {
syncState.assign({ sync });
}
else {
when(() => !local || syncState.isLoadedLocal.get(), sync);
}
}
when(!local || syncState.isLoadedLocal, function () {
obs.onChange(onObsChange.bind(this, obs, syncState, localState, persistOptions));
});
return obs;
}
function isInRemoteChange() {
return internal$1.globalState.isLoadingRemote;
}
const internal = {
observablePersistConfiguration,
};
export { configureObservablePersistence, internal, invertFieldMap, isInRemoteChange, mapPersistences, onChangeRemote, persistObservable, persistState, transformObject, transformPath };
//# sourceMappingURL=persist.mjs.map