@legendapp/state 0.23.2 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -0
  3. package/helpers/pageHash.js +1 -1
  4. package/helpers/pageHash.js.map +1 -1
  5. package/helpers/pageHash.mjs +1 -1
  6. package/helpers/pageHash.mjs.map +1 -1
  7. package/helpers/pageHashParams.js +1 -1
  8. package/helpers/pageHashParams.js.map +1 -1
  9. package/helpers/pageHashParams.mjs +1 -1
  10. package/helpers/pageHashParams.mjs.map +1 -1
  11. package/history.js +3 -3
  12. package/history.js.map +1 -1
  13. package/history.mjs +4 -4
  14. package/history.mjs.map +1 -1
  15. package/index.d.ts +2 -2
  16. package/index.js +99 -67
  17. package/index.js.map +1 -1
  18. package/index.mjs +97 -64
  19. package/index.mjs.map +1 -1
  20. package/package.json +1 -1
  21. package/persist-plugins/indexeddb-preloader.js +13 -11
  22. package/persist-plugins/indexeddb-preloader.js.map +1 -1
  23. package/persist-plugins/indexeddb-preloader.mjs +13 -11
  24. package/persist-plugins/indexeddb-preloader.mjs.map +1 -1
  25. package/persist-plugins/indexeddb.d.ts +3 -2
  26. package/persist-plugins/indexeddb.js +87 -57
  27. package/persist-plugins/indexeddb.js.map +1 -1
  28. package/persist-plugins/indexeddb.mjs +88 -58
  29. package/persist-plugins/indexeddb.mjs.map +1 -1
  30. package/persist-plugins/local-storage.d.ts +4 -3
  31. package/persist-plugins/local-storage.js +18 -8
  32. package/persist-plugins/local-storage.js.map +1 -1
  33. package/persist-plugins/local-storage.mjs +18 -8
  34. package/persist-plugins/local-storage.mjs.map +1 -1
  35. package/persist-plugins/mmkv.d.ts +3 -2
  36. package/persist-plugins/mmkv.js +27 -18
  37. package/persist-plugins/mmkv.js.map +1 -1
  38. package/persist-plugins/mmkv.mjs +27 -18
  39. package/persist-plugins/mmkv.mjs.map +1 -1
  40. package/persist.d.ts +3 -2
  41. package/persist.js +274 -112
  42. package/persist.js.map +1 -1
  43. package/persist.mjs +274 -113
  44. package/persist.mjs.map +1 -1
  45. package/react-hooks/usePersistedObservable.js.map +1 -1
  46. package/react-hooks/usePersistedObservable.mjs.map +1 -1
  47. package/react.js +1 -1
  48. package/react.js.map +1 -1
  49. package/react.mjs +2 -2
  50. package/react.mjs.map +1 -1
  51. package/src/batching.d.ts +2 -9
  52. package/src/helpers.d.ts +4 -4
  53. package/src/notify.d.ts +1 -1
  54. package/src/observableInterfaces.d.ts +56 -17
  55. package/src/onChange.d.ts +4 -1
  56. package/src/persist/fieldTransformer.d.ts +8 -3
  57. package/src/persist/persistHelpers.d.ts +2 -0
  58. package/src/persist/persistObservable.d.ts +7 -3
  59. package/src/persist-plugins/indexeddb.d.ts +3 -2
  60. package/src/persist-plugins/local-storage.d.ts +4 -3
  61. package/src/persist-plugins/mmkv.d.ts +3 -2
  62. package/trace.js.map +1 -1
  63. package/trace.mjs.map +1 -1
package/persist.mjs CHANGED
@@ -1,17 +1,18 @@
1
- import { isObject, observable, when, tracking as tracking$1, symbolDateModified, dateModifiedKey, constructObject, clone, deconstructObject, isEmpty, mergeIntoObservable, beginBatch, endBatch, batch, isString } from '@legendapp/state';
1
+ import { symbolDelete, symbolDateModified, isObject, isString, constructObjectWithPath, dateModifiedKey, deconstructObjectWithPath, isArray, observable, tracking as tracking$1, beginBatch, endBatch, when, isSymbol, isPromise, isEmpty, batch, mergeIntoObservable, setAtPath } from '@legendapp/state';
2
2
 
3
3
  const observablePersistConfiguration = {};
4
4
  function configureObservablePersistence(options) {
5
5
  Object.assign(observablePersistConfiguration, options);
6
6
  }
7
7
 
8
- function transformPath(path, map, passThroughKeys, ignoreKeys) {
8
+ let validateMap;
9
+ function transformPath(path, map, passThroughKeys) {
9
10
  const data = {};
10
11
  let d = data;
11
12
  for (let i = 0; i < path.length; i++) {
12
13
  d = d[path[i]] = i === path.length - 1 ? null : {};
13
14
  }
14
- let value = transformObject(data, map, passThroughKeys, ignoreKeys);
15
+ let value = transformObject(data, map, passThroughKeys);
15
16
  const pathOut = [];
16
17
  for (let i = 0; i < path.length; i++) {
17
18
  const key = Object.keys(value)[0];
@@ -21,11 +22,20 @@ function transformPath(path, map, passThroughKeys, ignoreKeys) {
21
22
  return pathOut;
22
23
  }
23
24
  function transformObject(dataIn, map, passThroughKeys, ignoreKeys) {
25
+ if (process.env.NODE_ENV === 'development') {
26
+ validateMap(map);
27
+ }
24
28
  // Note: If changing this, change it in IndexedDB preloader
25
29
  let ret = dataIn;
26
30
  if (dataIn) {
31
+ if (dataIn === symbolDelete)
32
+ return dataIn;
27
33
  ret = {};
28
34
  const dict = Object.keys(map).length === 1 && map['_dict'];
35
+ let dateModified = dataIn[symbolDateModified];
36
+ if (dateModified) {
37
+ ret[symbolDateModified] = dateModified;
38
+ }
29
39
  Object.keys(dataIn).forEach((key) => {
30
40
  if (ret[key] !== undefined || (ignoreKeys === null || ignoreKeys === void 0 ? void 0 : ignoreKeys.includes(key)))
31
41
  return;
@@ -47,14 +57,24 @@ function transformObject(dataIn, map, passThroughKeys, ignoreKeys) {
47
57
  }
48
58
  else if (mapped !== null) {
49
59
  if (v !== undefined && v !== null) {
50
- if (map[key + '_obj']) {
60
+ if (map[key + '_val']) {
61
+ const valMap = map[key + '_val'];
62
+ v = valMap[key];
63
+ }
64
+ else if (map[key + '_obj']) {
51
65
  v = transformObject(v, map[key + '_obj'], passThroughKeys, ignoreKeys);
52
66
  }
53
67
  else if (map[key + '_dict']) {
54
68
  const mapChild = map[key + '_dict'];
69
+ let out = {};
70
+ let dateModifiedChild = dataIn[symbolDateModified];
71
+ if (dateModifiedChild) {
72
+ out[symbolDateModified] = dateModifiedChild;
73
+ }
55
74
  Object.keys(v).forEach((keyChild) => {
56
- v[keyChild] = transformObject(v[keyChild], mapChild, passThroughKeys, ignoreKeys);
75
+ out[keyChild] = transformObject(v[keyChild], mapChild, passThroughKeys, ignoreKeys);
57
76
  });
77
+ v = out;
58
78
  }
59
79
  else if (map[key + '_arr']) {
60
80
  const mapChild = map[key + '_arr'];
@@ -72,8 +92,14 @@ function transformObject(dataIn, map, passThroughKeys, ignoreKeys) {
72
92
  debugger;
73
93
  return ret;
74
94
  }
95
+ function transformObjectWithPath(obj, path, pathTypes, fieldTransforms) {
96
+ let constructed = constructObjectWithPath(path, obj, pathTypes);
97
+ const transformed = transformObject(constructed, fieldTransforms, [dateModifiedKey]);
98
+ const transformedPath = transformPath(path, fieldTransforms, [dateModifiedKey]);
99
+ return { path: transformedPath, obj: deconstructObjectWithPath(transformedPath, transformed) };
100
+ }
75
101
  const invertedMaps = new WeakMap();
76
- function invertMap(obj) {
102
+ function invertFieldMap(obj) {
77
103
  // Note: If changing this, change it in IndexedDB preloader
78
104
  const existing = invertedMaps.get(obj);
79
105
  if (existing)
@@ -84,12 +110,12 @@ function invertMap(obj) {
84
110
  if (process.env.NODE_ENV === 'development' && target[val])
85
111
  debugger;
86
112
  if (key === '_dict') {
87
- target[key] = invertMap(val);
113
+ target[key] = invertFieldMap(val);
88
114
  }
89
115
  else if (key.endsWith('_obj') || key.endsWith('_dict') || key.endsWith('_arr')) {
90
116
  const keyMapped = obj[key.replace(/_obj|_dict|_arr$/, '')];
91
117
  const suffix = key.match(/_obj|_dict|_arr$/)[0];
92
- target[keyMapped + suffix] = invertMap(val);
118
+ target[keyMapped + suffix] = invertFieldMap(val);
93
119
  }
94
120
  else if (typeof val === 'string') {
95
121
  target[val] = key;
@@ -100,21 +126,25 @@ function invertMap(obj) {
100
126
  invertedMaps.set(obj, target);
101
127
  return target;
102
128
  }
103
-
104
- function removeNullUndefined(val) {
105
- if (val === undefined)
106
- return null;
107
- Object.keys(val).forEach((key) => {
108
- const v = val[key];
109
- if (v === null || v === undefined) {
110
- delete val[key];
111
- }
112
- else if (isObject(v)) {
113
- removeNullUndefined(v);
129
+ if (process.env.NODE_ENV === 'development') {
130
+ validateMap = function (record) {
131
+ const values = Object.values(record).filter((value) => {
132
+ if (isObject(value)) {
133
+ validateMap(value);
134
+ }
135
+ else {
136
+ return isString(value);
137
+ }
138
+ });
139
+ const uniques = Array.from(new Set(values));
140
+ if (values.length !== uniques.length) {
141
+ console.error('Field transform map has duplicate values', record, values.length, uniques.length);
142
+ debugger;
114
143
  }
115
- });
116
- return val;
144
+ return record;
145
+ };
117
146
  }
147
+
118
148
  function replaceKeyInObject(obj, keySource, keyTarget, clone) {
119
149
  if (isObject(obj)) {
120
150
  const target = clone ? {} : obj;
@@ -122,6 +152,9 @@ function replaceKeyInObject(obj, keySource, keyTarget, clone) {
122
152
  target[keyTarget] = obj[keySource];
123
153
  delete target[keySource];
124
154
  }
155
+ if (keySource !== symbolDateModified && obj[symbolDateModified]) {
156
+ target[symbolDateModified] = obj[symbolDateModified];
157
+ }
125
158
  Object.keys(obj).forEach((key) => {
126
159
  if (key !== keySource) {
127
160
  target[key] = replaceKeyInObject(obj[key], keySource, keyTarget, clone);
@@ -136,59 +169,134 @@ function replaceKeyInObject(obj, keySource, keyTarget, clone) {
136
169
  function getDateModifiedKey(dateModifiedKey) {
137
170
  return dateModifiedKey || observablePersistConfiguration.dateModifiedKey || '@';
138
171
  }
172
+ function mergeDateModified(obs, source) {
173
+ const isArr = isArray(source);
174
+ const isObj = !isArr && isObject(source);
175
+ let dateModified = isObj && source[symbolDateModified];
176
+ if (dateModified) {
177
+ delete source[symbolDateModified];
178
+ }
179
+ if (isArr || isObj) {
180
+ const keys = isArr ? source : Object.keys(source);
181
+ for (let i = 0; i < keys.length; i++) {
182
+ const key = isArr ? i : keys[i];
183
+ dateModified = Math.max(dateModified || 0, mergeDateModified(obs[key], source[key]));
184
+ }
185
+ }
186
+ if (dateModified) {
187
+ obs[symbolDateModified].set(dateModified);
188
+ }
189
+ return dateModified || 0;
190
+ }
139
191
 
140
192
  const mapPersistences = new WeakMap();
141
193
  const persistState = observable({ inRemoteSync: false });
142
194
  function parseLocalConfig(config) {
143
195
  return isString(config) ? { table: config, config: { name: config } } : { table: config.name, config };
144
196
  }
145
- async function onObsChange(obs, obsState, localState, persistOptions, value, getPrevious, changes) {
197
+ function adjustSaveData(value, path, pathTypes, { adjustData, fieldTransforms, }, replaceKey) {
198
+ let cloned = replaceKey ? replaceKeyInObject(value, symbolDateModified, dateModifiedKey, /*clone*/ true) : value;
199
+ const transform = () => {
200
+ if (fieldTransforms) {
201
+ const { obj, path: pathTransformed } = transformObjectWithPath(cloned, path, pathTypes, fieldTransforms);
202
+ cloned = obj;
203
+ path = pathTransformed;
204
+ }
205
+ return { value: cloned, path };
206
+ };
207
+ let promise;
208
+ if (adjustData === null || adjustData === void 0 ? void 0 : adjustData.save) {
209
+ const constructed = constructObjectWithPath(path, cloned, pathTypes);
210
+ promise = adjustData.save(constructed);
211
+ }
212
+ return isPromise(promise)
213
+ ? promise.then((adjusted) => {
214
+ cloned = deconstructObjectWithPath(path, adjusted);
215
+ return transform();
216
+ })
217
+ : transform();
218
+ }
219
+ function adjustLoadData(value, { adjustData, fieldTransforms, }, replaceKey) {
220
+ let cloned = replaceKey ? replaceKeyInObject(value, dateModifiedKey, symbolDateModified, /*clone*/ true) : value;
221
+ if (fieldTransforms) {
222
+ const inverted = invertFieldMap(fieldTransforms);
223
+ cloned = transformObject(cloned, inverted, [dateModifiedKey]);
224
+ }
225
+ if (adjustData === null || adjustData === void 0 ? void 0 : adjustData.load) {
226
+ cloned = adjustData.load(cloned);
227
+ }
228
+ return cloned;
229
+ }
230
+ async function onObsChange(obs, obsState, localState, persistOptions, { value, changes }) {
231
+ var _a;
146
232
  const { persistenceLocal, persistenceRemote } = localState;
147
233
  const local = persistOptions.local;
148
234
  const { table, config } = parseLocalConfig(local);
235
+ const configRemote = persistOptions.remote;
149
236
  const inRemoteChange = tracking$1.inRemoteChange;
150
- const saveRemote = !inRemoteChange && persistOptions.remote && !persistOptions.remote.readonly && obsState.isEnabledRemote.peek();
151
- if (local && obsState.isEnabledLocal.peek()) {
237
+ const saveRemote = !inRemoteChange && configRemote && !configRemote.readonly && obsState.isEnabledRemote.peek();
238
+ const isQueryingModified = !!((_a = configRemote === null || configRemote === void 0 ? void 0 : configRemote.firebase) === null || _a === void 0 ? void 0 : _a.queryByModified);
239
+ if (local && !config.readonly && obsState.isEnabledLocal.peek()) {
152
240
  if (!obsState.isLoadedLocal.peek()) {
153
241
  console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
154
242
  return;
155
243
  }
156
- // If saving remotely convert symbolDateModified to dateModifiedKey before saving locally
157
- // as persisting may not include symbols correctly
158
- let localValue = value;
159
- if (persistOptions.remote) {
160
- if (saveRemote) {
161
- for (let i = 0; i < changes.length; i++) {
162
- const { path, valueAtPath, prevAtPath } = changes[i];
163
- if (path[path.length - 1] === symbolDateModified)
164
- continue;
165
- const pathStr = path.join('/');
166
- if (!localState.pendingChanges) {
167
- localState.pendingChanges = {};
168
- }
169
- // The value saved in pending should be the previous state before changes,
170
- // so don't overwrite it if it already exists
171
- if (!localState.pendingChanges[pathStr]) {
172
- localState.pendingChanges[pathStr] = { p: prevAtPath !== null && prevAtPath !== void 0 ? prevAtPath : null };
173
- }
174
- localState.pendingChanges[pathStr].v = valueAtPath;
244
+ // Prepare pending changes
245
+ if (saveRemote) {
246
+ for (let i = 0; i < changes.length; i++) {
247
+ const { path, valueAtPath, prevAtPath, pathTypes } = changes[i];
248
+ if (path[path.length - 1] === symbolDateModified)
249
+ continue;
250
+ const pathStr = path.join('/');
251
+ if (!localState.pendingChanges) {
252
+ localState.pendingChanges = {};
175
253
  }
254
+ // The value saved in pending should be the previous state before changes,
255
+ // so don't overwrite it if it already exists
256
+ if (!localState.pendingChanges[pathStr]) {
257
+ localState.pendingChanges[pathStr] = { p: prevAtPath !== null && prevAtPath !== void 0 ? prevAtPath : null, t: pathTypes };
258
+ }
259
+ localState.pendingChanges[pathStr].v = valueAtPath;
176
260
  }
177
261
  }
178
- let changesLocal = changes;
179
- if (config.fieldTransforms) {
180
- localValue = transformObject(localValue, config.fieldTransforms, [dateModifiedKey]);
181
- changesLocal = changesLocal.map(({ path, prevAtPath, valueAtPath }) => {
182
- let transformed = constructObject(path, clone(valueAtPath));
183
- transformed = transformObject(transformed, config.fieldTransforms, [dateModifiedKey]);
184
- const transformedPath = transformPath(path, config.fieldTransforms, [dateModifiedKey]);
185
- const toSave = deconstructObject(transformedPath, transformed);
186
- return { path, prevAtPath, valueAtPath: toSave };
187
- });
262
+ // Save changes locally
263
+ const changesLocal = [];
264
+ const changesPaths = new Set();
265
+ const promises = [];
266
+ changes.forEach((_, i) => {
267
+ // Reverse order
268
+ let { path: pathOriginal, prevAtPath, valueAtPath, pathTypes } = changes[changes.length - 1 - i];
269
+ if (isSymbol(pathOriginal[pathOriginal.length - 1])) {
270
+ return;
271
+ }
272
+ if (isQueryingModified) {
273
+ pathOriginal = pathOriginal.map((p) => (p === symbolDateModified ? dateModifiedKey : p));
274
+ }
275
+ const pathStr = pathOriginal.join('/');
276
+ // Optimization to only save the latest update at each path. We might have multiple changes at the same path
277
+ // and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
278
+ // already processed.
279
+ if (!changesPaths.has(pathStr)) {
280
+ changesPaths.add(pathStr);
281
+ let promise = adjustSaveData(valueAtPath, pathOriginal, pathTypes, config, isQueryingModified);
282
+ const push = ({ path, value }) => {
283
+ changesLocal.push({ path, pathTypes, prevAtPath, valueAtPath: value });
284
+ };
285
+ if (isPromise(promise)) {
286
+ promises.push(promise.then(push));
287
+ }
288
+ else {
289
+ push(promise);
290
+ }
291
+ }
292
+ });
293
+ if (promises.length > 0) {
294
+ await Promise.all(promises);
188
295
  }
189
- localValue = replaceKeyInObject(localValue, symbolDateModified, dateModifiedKey,
190
- /*clone*/ true);
191
- persistenceLocal.set(table, localValue, changesLocal, config);
296
+ if (changesLocal.length > 0) {
297
+ persistenceLocal.set(table, changesLocal, config);
298
+ }
299
+ // Save metadata
192
300
  const metadata = {};
193
301
  if (inRemoteChange) {
194
302
  const dateModified = value[symbolDateModified];
@@ -204,49 +312,55 @@ async function onObsChange(obs, obsState, localState, persistOptions, value, get
204
312
  }
205
313
  }
206
314
  if (saveRemote) {
207
- await when(obsState.isLoadedRemote);
208
- for (let i = 0; i < changes.length; i++) {
209
- const { path, valueAtPath, prevAtPath } = changes[i];
315
+ await when(() => obsState.isLoadedRemote.get() || (configRemote.allowSaveIfError && obsState.remoteError.get()));
316
+ const fieldTransforms = configRemote.fieldTransforms;
317
+ changes.forEach(async (change) => {
318
+ const { path, valueAtPath, prevAtPath, pathTypes } = change;
210
319
  if (path[path.length - 1] === symbolDateModified)
211
- continue;
320
+ return;
212
321
  const pathStr = path.join('/');
322
+ const { path: pathSave, value: valueSave } = await adjustSaveData(valueAtPath, path, pathTypes, configRemote, isQueryingModified);
213
323
  // Save to remote persistence and get the remote value from it. Some providers (like Firebase) will return a
214
- // server value different than the saved value (like Firebase has server timestamps for dateModified)
215
- persistenceRemote.save(persistOptions, value, path, valueAtPath, prevAtPath).then((saved) => {
324
+ // server value with server timestamps for dateModified.
325
+ persistenceRemote
326
+ .save({
327
+ obs,
328
+ state: obsState,
329
+ options: persistOptions,
330
+ path: pathSave,
331
+ pathTypes,
332
+ valueAtPath: valueSave,
333
+ prevAtPath,
334
+ })
335
+ .then((saved) => {
216
336
  var _a;
217
337
  if (local) {
218
- let toSave = persistenceLocal.getTable(table, config);
219
338
  const pending = (_a = persistenceLocal.getMetadata(table, config)) === null || _a === void 0 ? void 0 : _a.pending;
220
- let dateModified;
221
- let didDelete = false;
339
+ // Clear pending for this path
222
340
  if (pending === null || pending === void 0 ? void 0 : pending[pathStr]) {
223
- didDelete = true;
224
341
  // Remove pending from the saved object
225
342
  delete pending[pathStr];
226
343
  // Remove pending from local state
227
344
  delete localState.pendingChanges[pathStr];
345
+ persistenceLocal.updateMetadata(table, { pending }, config);
228
346
  }
229
347
  // Only the latest save will return a value so that it saves back to local persistence once
230
- if (saved !== undefined) {
348
+ // It needs to get the dateModified from the save and update that through the observable
349
+ // which will fire onObsChange and save it locally.
350
+ if (saved !== undefined && isQueryingModified) {
351
+ // Note: Don't need to adjust data because we're just merging dateModified
352
+ const invertedMap = fieldTransforms && invertFieldMap(fieldTransforms);
353
+ if (invertedMap) {
354
+ saved = transformObject(saved, invertedMap, [dateModifiedKey]);
355
+ }
231
356
  onChangeRemote(() => {
232
- mergeIntoObservable(obs, saved);
357
+ mergeDateModified(obs, saved);
233
358
  });
234
- dateModified = saved[symbolDateModified];
235
- // Replace the dateModifiedKey and remove null/undefined before saving
236
- if (config.fieldTransforms) {
237
- saved = transformObject(saved, config.fieldTransforms, [dateModifiedKey]);
238
- }
239
- saved = replaceKeyInObject(removeNullUndefined(saved), symbolDateModified, dateModifiedKey,
240
- /*clone*/ false);
241
- toSave = toSave ? mergeIntoObservable(toSave, saved) : saved;
242
- }
243
- if (saved !== undefined || didDelete) {
244
- persistenceLocal.set(table, toSave, [changes[i]], config);
245
- persistenceLocal.updateMetadata(table, { pending, modified: dateModified }, config);
246
359
  }
247
360
  }
361
+ localState.onSaveRemoteListeners.forEach((cb) => cb());
248
362
  });
249
- }
363
+ });
250
364
  }
251
365
  }
252
366
  function onChangeRemote(cb) {
@@ -264,23 +378,33 @@ function onChangeRemote(cb) {
264
378
  }
265
379
  }
266
380
  async function loadLocal(obs, persistOptions, obsState, localState) {
267
- var _a;
381
+ var _a, _b, _c, _d;
268
382
  const { local, remote } = persistOptions;
269
383
  const localPersistence = persistOptions.persistLocal || observablePersistConfiguration.persistLocal;
270
384
  if (local) {
271
385
  const { table, config } = parseLocalConfig(local);
386
+ const isQueryingModified = !!((_b = (_a = persistOptions.remote) === null || _a === void 0 ? void 0 : _a.firebase) === null || _b === void 0 ? void 0 : _b.queryByModified);
272
387
  if (!localPersistence) {
273
388
  throw new Error('Local persistence is not configured');
274
389
  }
275
390
  // Ensure there's only one instance of the persistence plugin
276
391
  if (!mapPersistences.has(localPersistence)) {
277
392
  const persistenceLocal = new localPersistence();
393
+ const mapValue = { persist: persistenceLocal, initialized: observable(false) };
394
+ mapPersistences.set(localPersistence, mapValue);
278
395
  if (persistenceLocal.initialize) {
279
- await ((_a = persistenceLocal.initialize) === null || _a === void 0 ? void 0 : _a.call(persistenceLocal, observablePersistConfiguration.persistLocalOptions));
396
+ const initializePromise = (_c = persistenceLocal.initialize) === null || _c === void 0 ? void 0 : _c.call(persistenceLocal, observablePersistConfiguration.persistLocalOptions);
397
+ if (isPromise(initializePromise)) {
398
+ await initializePromise;
399
+ }
280
400
  }
281
- mapPersistences.set(localPersistence, persistenceLocal);
401
+ mapValue.initialized.set(true);
402
+ }
403
+ const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
404
+ localState.persistenceLocal = persistenceLocal;
405
+ if (!initialized.get()) {
406
+ await when(initialized);
282
407
  }
283
- const persistenceLocal = (localState.persistenceLocal = mapPersistences.get(localPersistence));
284
408
  // If persistence has an asynchronous load, wait for it
285
409
  if (persistenceLocal.loadTable) {
286
410
  const promise = persistenceLocal.loadTable(table, config);
@@ -291,36 +415,37 @@ async function loadLocal(obs, persistOptions, obsState, localState) {
291
415
  // Get the value from state
292
416
  let value = persistenceLocal.getTable(table, config);
293
417
  const metadata = persistenceLocal.getMetadata(table, config);
294
- if (config.fieldTransforms) {
295
- // Get preloaded translated if available
296
- let valueLoaded = persistenceLocal.getTableTransformed(table, config);
297
- if (valueLoaded) {
298
- value = valueLoaded;
299
- }
300
- else {
301
- const inverted = invertMap(config.fieldTransforms);
302
- value = transformObject(value, inverted, [dateModifiedKey]);
303
- }
304
- }
305
418
  if (metadata) {
306
419
  const pending = metadata.pending;
307
420
  localState.pendingChanges = pending;
308
421
  }
309
422
  // Merge the data from local persistence into the default state
310
423
  if (value !== null && value !== undefined) {
311
- if (remote) {
312
- replaceKeyInObject(value, dateModifiedKey, symbolDateModified, /*clone*/ false);
424
+ let { adjustData, fieldTransforms } = config;
425
+ if (fieldTransforms) {
426
+ let valueLoaded = (_d = persistenceLocal.getTableTransformed) === null || _d === void 0 ? void 0 : _d.call(persistenceLocal, table, config);
427
+ if (valueLoaded) {
428
+ value = valueLoaded;
429
+ fieldTransforms = undefined;
430
+ }
313
431
  }
314
- if (metadata === null || metadata === void 0 ? void 0 : metadata.modified) {
315
- value[symbolDateModified] = metadata.modified;
432
+ value = adjustLoadData(value, { adjustData, fieldTransforms }, !!remote && isQueryingModified);
433
+ if (isPromise(value)) {
434
+ value = await value;
316
435
  }
317
- batch(() => mergeIntoObservable(obs, value));
436
+ batch(() => {
437
+ mergeIntoObservable(obs, value);
438
+ });
318
439
  }
319
440
  obsState.peek().clearLocal = () => persistenceLocal.deleteTable(table, config);
320
441
  }
321
442
  obsState.isLoadedLocal.set(true);
322
443
  }
323
444
  function persistObservable(obs, persistOptions) {
445
+ const { remote, local } = persistOptions;
446
+ const remotePersistence = persistOptions.persistRemote || (observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.persistRemote);
447
+ const onSaveRemoteListeners = [];
448
+ const localState = { onSaveRemoteListeners };
324
449
  const obsState = observable({
325
450
  isLoadedLocal: false,
326
451
  isLoadedRemote: false,
@@ -328,10 +453,8 @@ function persistObservable(obs, persistOptions) {
328
453
  isEnabledRemote: true,
329
454
  clearLocal: undefined,
330
455
  sync: () => Promise.resolve(),
456
+ getPendingChanges: () => localState.pendingChanges,
331
457
  });
332
- const { remote, local } = persistOptions;
333
- const remotePersistence = persistOptions.persistRemote || (observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.persistRemote);
334
- const localState = {};
335
458
  if (local) {
336
459
  loadLocal(obs, persistOptions, obsState, localState);
337
460
  }
@@ -341,24 +464,62 @@ function persistObservable(obs, persistOptions) {
341
464
  }
342
465
  // Ensure there's only one instance of the persistence plugin
343
466
  if (!mapPersistences.has(remotePersistence)) {
344
- mapPersistences.set(remotePersistence, new remotePersistence());
467
+ mapPersistences.set(remotePersistence, { persist: new remotePersistence() });
345
468
  }
346
- localState.persistenceRemote = mapPersistences.get(remotePersistence);
469
+ localState.persistenceRemote = mapPersistences.get(remotePersistence).persist;
347
470
  let isSynced = false;
348
471
  const sync = async () => {
472
+ var _a;
349
473
  if (!isSynced) {
350
474
  isSynced = true;
351
- localState.persistenceRemote.listen(obs, persistOptions, () => {
352
- obsState.isLoadedRemote.set(true);
353
- }, onChangeRemote);
354
- await when(obsState.isLoadedRemote);
475
+ const onSaveRemote = (_a = persistOptions.remote) === null || _a === void 0 ? void 0 : _a.onSaveRemote;
476
+ if (onSaveRemote) {
477
+ onSaveRemoteListeners.push(onSaveRemote);
478
+ }
479
+ localState.persistenceRemote.listen({
480
+ state: obsState,
481
+ obs,
482
+ options: persistOptions,
483
+ onLoad: () => {
484
+ obsState.isLoadedRemote.set(true);
485
+ },
486
+ onChange: async ({ value, path, mode }) => {
487
+ if (value !== undefined) {
488
+ value = adjustLoadData(value, remote, true);
489
+ if (isPromise(value)) {
490
+ value = await value;
491
+ }
492
+ const pending = localState.pendingChanges;
493
+ if (pending) {
494
+ Object.keys(pending).forEach((key) => {
495
+ const p = key.split('/').filter((p) => p !== '');
496
+ const { v, t } = pending[key];
497
+ const constructed = constructObjectWithPath(p, v, t);
498
+ value = mergeIntoObservable(value, constructed);
499
+ });
500
+ }
501
+ const invertedMap = remote.fieldTransforms && invertFieldMap(remote.fieldTransforms);
502
+ if (path.length && invertedMap) {
503
+ path = transformPath(path, invertedMap);
504
+ }
505
+ onChangeRemote(() => {
506
+ setAtPath(obs, path, value, mode);
507
+ });
508
+ }
509
+ },
510
+ });
511
+ await when(() => obsState.isLoadedRemote.get() || (remote.allowSaveIfError && obsState.remoteError.get()));
355
512
  const pending = localState.pendingChanges;
356
513
  if (pending) {
357
514
  Object.keys(pending).forEach((key) => {
358
515
  const path = key.split('/').filter((p) => p !== '');
359
- const { p, v } = pending[key];
516
+ const { p, v, t } = pending[key];
360
517
  // TODO getPrevious if any remote persistence layers need it
361
- onObsChange(obs, obsState, localState, persistOptions, obs.peek(), () => undefined, [{ path, valueAtPath: v, prevAtPath: p }]);
518
+ onObsChange(obs, obsState, localState, persistOptions, {
519
+ value: obs.peek(),
520
+ getPrevious: () => undefined,
521
+ changes: [{ path, valueAtPath: v, prevAtPath: p, pathTypes: t }],
522
+ });
362
523
  });
363
524
  }
364
525
  }
@@ -385,5 +546,5 @@ function isInRemoteChange() {
385
546
  return tracking.inRemoteChange;
386
547
  }
387
548
 
388
- export { configureObservablePersistence, getDateModifiedKey, invertMap, isInRemoteChange, mapPersistences, observablePersistConfiguration, persistObservable, persistState, transformObject, transformPath };
549
+ export { configureObservablePersistence, getDateModifiedKey, invertFieldMap, isInRemoteChange, mapPersistences, observablePersistConfiguration, onChangeRemote, persistObservable, persistState, transformObject, transformPath };
389
550
  //# sourceMappingURL=persist.mjs.map