@legendapp/state 0.23.2 → 1.0.0-rc.2

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 +25 -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 +151 -81
  17. package/index.js.map +1 -1
  18. package/index.mjs +148 -78
  19. package/index.mjs.map +1 -1
  20. package/package.json +1 -1
  21. package/persist-plugins/indexeddb-preloader.js +16 -10
  22. package/persist-plugins/indexeddb-preloader.js.map +1 -1
  23. package/persist-plugins/indexeddb-preloader.mjs +16 -10
  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 +92 -56
  27. package/persist-plugins/indexeddb.js.map +1 -1
  28. package/persist-plugins/indexeddb.mjs +93 -57
  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 +292 -113
  42. package/persist.js.map +1 -1
  43. package/persist.mjs +292 -114
  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 +3 -10
  52. package/src/helpers.d.ts +5 -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, setInObservableAtPath } 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,139 @@ 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.peek()[symbolDateModified] = 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) {
146
- const { persistenceLocal, persistenceRemote } = localState;
197
+ let isMergingLocalData = false;
198
+ function adjustSaveData(value, path, pathTypes, { adjustData, fieldTransforms, }, replaceKey) {
199
+ let cloned = replaceKey ? replaceKeyInObject(value, symbolDateModified, dateModifiedKey, /*clone*/ true) : value;
200
+ const transform = () => {
201
+ if (fieldTransforms) {
202
+ const { obj, path: pathTransformed } = transformObjectWithPath(cloned, path, pathTypes, fieldTransforms);
203
+ cloned = obj;
204
+ path = pathTransformed;
205
+ }
206
+ return { value: cloned, path };
207
+ };
208
+ let promise;
209
+ if (adjustData === null || adjustData === void 0 ? void 0 : adjustData.save) {
210
+ const constructed = constructObjectWithPath(path, cloned, pathTypes);
211
+ promise = adjustData.save(constructed);
212
+ }
213
+ return isPromise(promise)
214
+ ? promise.then((adjusted) => {
215
+ cloned = deconstructObjectWithPath(path, adjusted);
216
+ return transform();
217
+ })
218
+ : transform();
219
+ }
220
+ function adjustLoadData(value, { adjustData, fieldTransforms, }, replaceKey) {
221
+ let cloned = replaceKey ? replaceKeyInObject(value, dateModifiedKey, symbolDateModified, /*clone*/ true) : value;
222
+ if (fieldTransforms) {
223
+ const inverted = invertFieldMap(fieldTransforms);
224
+ cloned = transformObject(cloned, inverted, [dateModifiedKey]);
225
+ }
226
+ if (adjustData === null || adjustData === void 0 ? void 0 : adjustData.load) {
227
+ cloned = adjustData.load(cloned);
228
+ }
229
+ return cloned;
230
+ }
231
+ async function onObsChange(obs, obsState, localState, persistOptions, { value, changes }) {
232
+ var _a;
233
+ const { persistenceLocal, persistenceRemote, isApplyingPending } = localState;
147
234
  const local = persistOptions.local;
148
235
  const { table, config } = parseLocalConfig(local);
236
+ const configRemote = persistOptions.remote;
149
237
  const inRemoteChange = tracking$1.inRemoteChange;
150
- const saveRemote = !inRemoteChange && persistOptions.remote && !persistOptions.remote.readonly && obsState.isEnabledRemote.peek();
151
- if (local && obsState.isEnabledLocal.peek()) {
238
+ const saveRemote = !isMergingLocalData &&
239
+ !inRemoteChange &&
240
+ configRemote &&
241
+ !configRemote.readonly &&
242
+ obsState.isEnabledRemote.peek();
243
+ const isQueryingModified = !!((_a = configRemote === null || configRemote === void 0 ? void 0 : configRemote.firebase) === null || _a === void 0 ? void 0 : _a.queryByModified);
244
+ if (local && !config.readonly && !isApplyingPending && obsState.isEnabledLocal.peek()) {
152
245
  if (!obsState.isLoadedLocal.peek()) {
153
246
  console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
154
247
  return;
155
248
  }
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;
249
+ // Prepare pending changes
250
+ if (saveRemote) {
251
+ for (let i = 0; i < changes.length; i++) {
252
+ const { path, valueAtPath, prevAtPath, pathTypes } = changes[i];
253
+ if (path[path.length - 1] === symbolDateModified)
254
+ continue;
255
+ const pathStr = path.join('/');
256
+ if (!localState.pendingChanges) {
257
+ localState.pendingChanges = {};
175
258
  }
259
+ // The value saved in pending should be the previous state before changes,
260
+ // so don't overwrite it if it already exists
261
+ if (!localState.pendingChanges[pathStr]) {
262
+ localState.pendingChanges[pathStr] = { p: prevAtPath !== null && prevAtPath !== void 0 ? prevAtPath : null, t: pathTypes };
263
+ }
264
+ localState.pendingChanges[pathStr].v = valueAtPath;
176
265
  }
177
266
  }
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
- });
267
+ // Save changes locally
268
+ const changesLocal = [];
269
+ const changesPaths = new Set();
270
+ const promises = [];
271
+ changes.forEach((_, i) => {
272
+ // Reverse order
273
+ let { path: pathOriginal, prevAtPath, valueAtPath, pathTypes } = changes[changes.length - 1 - i];
274
+ if (isSymbol(pathOriginal[pathOriginal.length - 1])) {
275
+ return;
276
+ }
277
+ if (isQueryingModified) {
278
+ pathOriginal = pathOriginal.map((p) => (p === symbolDateModified ? dateModifiedKey : p));
279
+ }
280
+ const pathStr = pathOriginal.join('/');
281
+ // Optimization to only save the latest update at each path. We might have multiple changes at the same path
282
+ // and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
283
+ // already processed.
284
+ if (!changesPaths.has(pathStr)) {
285
+ changesPaths.add(pathStr);
286
+ let promise = adjustSaveData(valueAtPath, pathOriginal, pathTypes, config, isQueryingModified);
287
+ const push = ({ path, value }) => {
288
+ changesLocal.push({ path, pathTypes, prevAtPath, valueAtPath: value });
289
+ };
290
+ if (isPromise(promise)) {
291
+ promises.push(promise.then(push));
292
+ }
293
+ else {
294
+ push(promise);
295
+ }
296
+ }
297
+ });
298
+ if (promises.length > 0) {
299
+ await Promise.all(promises);
188
300
  }
189
- localValue = replaceKeyInObject(localValue, symbolDateModified, dateModifiedKey,
190
- /*clone*/ true);
191
- persistenceLocal.set(table, localValue, changesLocal, config);
301
+ if (changesLocal.length > 0) {
302
+ persistenceLocal.set(table, changesLocal, config);
303
+ }
304
+ // Save metadata
192
305
  const metadata = {};
193
306
  if (inRemoteChange) {
194
307
  const dateModified = value[symbolDateModified];
@@ -204,49 +317,55 @@ async function onObsChange(obs, obsState, localState, persistOptions, value, get
204
317
  }
205
318
  }
206
319
  if (saveRemote) {
207
- await when(obsState.isLoadedRemote);
208
- for (let i = 0; i < changes.length; i++) {
209
- const { path, valueAtPath, prevAtPath } = changes[i];
320
+ await when(() => obsState.isLoadedRemote.get() || (configRemote.allowSaveIfError && obsState.remoteError.get()));
321
+ const fieldTransforms = configRemote.fieldTransforms;
322
+ changes.forEach(async (change) => {
323
+ const { path, valueAtPath, prevAtPath, pathTypes } = change;
210
324
  if (path[path.length - 1] === symbolDateModified)
211
- continue;
325
+ return;
212
326
  const pathStr = path.join('/');
327
+ const { path: pathSave, value: valueSave } = await adjustSaveData(valueAtPath, path, pathTypes, configRemote, isQueryingModified);
213
328
  // 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) => {
329
+ // server value with server timestamps for dateModified.
330
+ persistenceRemote
331
+ .save({
332
+ obs,
333
+ state: obsState,
334
+ options: persistOptions,
335
+ path: pathSave,
336
+ pathTypes,
337
+ valueAtPath: valueSave,
338
+ prevAtPath,
339
+ })
340
+ .then((saved) => {
216
341
  var _a;
217
342
  if (local) {
218
- let toSave = persistenceLocal.getTable(table, config);
219
343
  const pending = (_a = persistenceLocal.getMetadata(table, config)) === null || _a === void 0 ? void 0 : _a.pending;
220
- let dateModified;
221
- let didDelete = false;
344
+ // Clear pending for this path
222
345
  if (pending === null || pending === void 0 ? void 0 : pending[pathStr]) {
223
- didDelete = true;
224
346
  // Remove pending from the saved object
225
347
  delete pending[pathStr];
226
348
  // Remove pending from local state
227
349
  delete localState.pendingChanges[pathStr];
350
+ persistenceLocal.updateMetadata(table, { pending }, config);
228
351
  }
229
352
  // Only the latest save will return a value so that it saves back to local persistence once
230
- if (saved !== undefined) {
353
+ // It needs to get the dateModified from the save and update that through the observable
354
+ // which will fire onObsChange and save it locally.
355
+ if (saved !== undefined && isQueryingModified) {
356
+ // Note: Don't need to adjust data because we're just merging dateModified
357
+ const invertedMap = fieldTransforms && invertFieldMap(fieldTransforms);
358
+ if (invertedMap) {
359
+ saved = transformObject(saved, invertedMap, [dateModifiedKey]);
360
+ }
231
361
  onChangeRemote(() => {
232
- mergeIntoObservable(obs, saved);
362
+ mergeDateModified(obs, saved);
233
363
  });
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
364
  }
247
365
  }
366
+ localState.onSaveRemoteListeners.forEach((cb) => cb());
248
367
  });
249
- }
368
+ });
250
369
  }
251
370
  }
252
371
  function onChangeRemote(cb) {
@@ -264,23 +383,33 @@ function onChangeRemote(cb) {
264
383
  }
265
384
  }
266
385
  async function loadLocal(obs, persistOptions, obsState, localState) {
267
- var _a;
386
+ var _a, _b, _c, _d;
268
387
  const { local, remote } = persistOptions;
269
388
  const localPersistence = persistOptions.persistLocal || observablePersistConfiguration.persistLocal;
270
389
  if (local) {
271
390
  const { table, config } = parseLocalConfig(local);
391
+ const isQueryingModified = !!((_b = (_a = persistOptions.remote) === null || _a === void 0 ? void 0 : _a.firebase) === null || _b === void 0 ? void 0 : _b.queryByModified);
272
392
  if (!localPersistence) {
273
393
  throw new Error('Local persistence is not configured');
274
394
  }
275
395
  // Ensure there's only one instance of the persistence plugin
276
396
  if (!mapPersistences.has(localPersistence)) {
277
397
  const persistenceLocal = new localPersistence();
398
+ const mapValue = { persist: persistenceLocal, initialized: observable(false) };
399
+ mapPersistences.set(localPersistence, mapValue);
278
400
  if (persistenceLocal.initialize) {
279
- await ((_a = persistenceLocal.initialize) === null || _a === void 0 ? void 0 : _a.call(persistenceLocal, observablePersistConfiguration.persistLocalOptions));
401
+ const initializePromise = (_c = persistenceLocal.initialize) === null || _c === void 0 ? void 0 : _c.call(persistenceLocal, observablePersistConfiguration.persistLocalOptions);
402
+ if (isPromise(initializePromise)) {
403
+ await initializePromise;
404
+ }
280
405
  }
281
- mapPersistences.set(localPersistence, persistenceLocal);
406
+ mapValue.initialized.set(true);
407
+ }
408
+ const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
409
+ localState.persistenceLocal = persistenceLocal;
410
+ if (!initialized.get()) {
411
+ await when(initialized);
282
412
  }
283
- const persistenceLocal = (localState.persistenceLocal = mapPersistences.get(localPersistence));
284
413
  // If persistence has an asynchronous load, wait for it
285
414
  if (persistenceLocal.loadTable) {
286
415
  const promise = persistenceLocal.loadTable(table, config);
@@ -291,36 +420,46 @@ async function loadLocal(obs, persistOptions, obsState, localState) {
291
420
  // Get the value from state
292
421
  let value = persistenceLocal.getTable(table, config);
293
422
  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
423
  if (metadata) {
306
424
  const pending = metadata.pending;
307
425
  localState.pendingChanges = pending;
308
426
  }
309
427
  // Merge the data from local persistence into the default state
310
428
  if (value !== null && value !== undefined) {
311
- if (remote) {
312
- replaceKeyInObject(value, dateModifiedKey, symbolDateModified, /*clone*/ false);
429
+ let { adjustData, fieldTransforms } = config;
430
+ if (fieldTransforms) {
431
+ let valueLoaded = (_d = persistenceLocal.getTableTransformed) === null || _d === void 0 ? void 0 : _d.call(persistenceLocal, table, config);
432
+ if (valueLoaded) {
433
+ value = valueLoaded;
434
+ fieldTransforms = undefined;
435
+ }
313
436
  }
314
- if (metadata === null || metadata === void 0 ? void 0 : metadata.modified) {
315
- value[symbolDateModified] = metadata.modified;
437
+ value = adjustLoadData(value, { adjustData, fieldTransforms }, !!remote && isQueryingModified);
438
+ if (isPromise(value)) {
439
+ value = await value;
316
440
  }
317
- batch(() => mergeIntoObservable(obs, value));
441
+ batch(() => {
442
+ // isMergingLocalData prevents saving remotely when two different persistences
443
+ // are set on the same observable
444
+ isMergingLocalData = true;
445
+ // We want to merge the local data on top of any initial state the object is created with
446
+ mergeIntoObservable(obs, value);
447
+ if (metadata === null || metadata === void 0 ? void 0 : metadata.modified) {
448
+ obs.peek()[symbolDateModified] = metadata.modified;
449
+ }
450
+ }, () => {
451
+ isMergingLocalData = false;
452
+ });
318
453
  }
319
454
  obsState.peek().clearLocal = () => persistenceLocal.deleteTable(table, config);
320
455
  }
321
456
  obsState.isLoadedLocal.set(true);
322
457
  }
323
458
  function persistObservable(obs, persistOptions) {
459
+ const { remote, local } = persistOptions;
460
+ const remotePersistence = persistOptions.persistRemote || (observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.persistRemote);
461
+ const onSaveRemoteListeners = [];
462
+ const localState = { onSaveRemoteListeners };
324
463
  const obsState = observable({
325
464
  isLoadedLocal: false,
326
465
  isLoadedRemote: false,
@@ -328,10 +467,8 @@ function persistObservable(obs, persistOptions) {
328
467
  isEnabledRemote: true,
329
468
  clearLocal: undefined,
330
469
  sync: () => Promise.resolve(),
470
+ getPendingChanges: () => localState.pendingChanges,
331
471
  });
332
- const { remote, local } = persistOptions;
333
- const remotePersistence = persistOptions.persistRemote || (observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.persistRemote);
334
- const localState = {};
335
472
  if (local) {
336
473
  loadLocal(obs, persistOptions, obsState, localState);
337
474
  }
@@ -341,25 +478,66 @@ function persistObservable(obs, persistOptions) {
341
478
  }
342
479
  // Ensure there's only one instance of the persistence plugin
343
480
  if (!mapPersistences.has(remotePersistence)) {
344
- mapPersistences.set(remotePersistence, new remotePersistence());
481
+ mapPersistences.set(remotePersistence, { persist: new remotePersistence() });
345
482
  }
346
- localState.persistenceRemote = mapPersistences.get(remotePersistence);
483
+ localState.persistenceRemote = mapPersistences.get(remotePersistence).persist;
347
484
  let isSynced = false;
348
485
  const sync = async () => {
486
+ var _a;
349
487
  if (!isSynced) {
350
488
  isSynced = true;
351
- localState.persistenceRemote.listen(obs, persistOptions, () => {
352
- obsState.isLoadedRemote.set(true);
353
- }, onChangeRemote);
354
- await when(obsState.isLoadedRemote);
489
+ const onSaveRemote = (_a = persistOptions.remote) === null || _a === void 0 ? void 0 : _a.onSaveRemote;
490
+ if (onSaveRemote) {
491
+ onSaveRemoteListeners.push(onSaveRemote);
492
+ }
493
+ localState.persistenceRemote.listen({
494
+ state: obsState,
495
+ obs,
496
+ options: persistOptions,
497
+ onLoad: () => {
498
+ obsState.isLoadedRemote.set(true);
499
+ },
500
+ onChange: async ({ value, path, mode }) => {
501
+ if (value !== undefined) {
502
+ value = adjustLoadData(value, remote, true);
503
+ if (isPromise(value)) {
504
+ value = await value;
505
+ }
506
+ const pending = localState.pendingChanges;
507
+ if (pending) {
508
+ Object.keys(pending).forEach((key) => {
509
+ const p = key.split('/').filter((p) => p !== '');
510
+ const { v, t } = pending[key];
511
+ const constructed = constructObjectWithPath(p, v, t);
512
+ value = mergeIntoObservable(value, constructed);
513
+ });
514
+ }
515
+ const invertedMap = remote.fieldTransforms && invertFieldMap(remote.fieldTransforms);
516
+ if (path.length && invertedMap) {
517
+ path = transformPath(path, invertedMap);
518
+ }
519
+ onChangeRemote(() => {
520
+ setInObservableAtPath(obs, path, value, mode);
521
+ mergeDateModified(obs, value);
522
+ });
523
+ }
524
+ },
525
+ });
526
+ await when(() => obsState.isLoadedRemote.get() || (remote.allowSaveIfError && obsState.remoteError.get()));
355
527
  const pending = localState.pendingChanges;
356
528
  if (pending) {
529
+ localState.isApplyingPending = true;
357
530
  Object.keys(pending).forEach((key) => {
358
531
  const path = key.split('/').filter((p) => p !== '');
359
- const { p, v } = pending[key];
532
+ const { p, v, t } = pending[key];
360
533
  // TODO getPrevious if any remote persistence layers need it
361
- onObsChange(obs, obsState, localState, persistOptions, obs.peek(), () => undefined, [{ path, valueAtPath: v, prevAtPath: p }]);
534
+ onObsChange(obs, obsState, localState, persistOptions, {
535
+ value: obs.peek(),
536
+ getPrevious: () => undefined,
537
+ changes: [{ path, valueAtPath: v, prevAtPath: p, pathTypes: t }],
538
+ });
362
539
  });
540
+ localState.isApplyingPending = false;
363
541
  }
364
542
  }
365
543
  };
@@ -385,5 +563,5 @@ function isInRemoteChange() {
385
563
  return tracking.inRemoteChange;
386
564
  }
387
565
 
388
- export { configureObservablePersistence, getDateModifiedKey, invertMap, isInRemoteChange, mapPersistences, observablePersistConfiguration, persistObservable, persistState, transformObject, transformPath };
566
+ export { configureObservablePersistence, getDateModifiedKey, invertFieldMap, isInRemoteChange, mapPersistences, observablePersistConfiguration, onChangeRemote, persistObservable, persistState, transformObject, transformPath };
389
567
  //# sourceMappingURL=persist.mjs.map