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