@legendapp/state 2.0.0-next.13 → 2.0.0-next.14
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.
- package/index.d.ts +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/index.mjs +1 -1
- package/index.mjs.map +1 -1
- package/package.json +6 -1
- package/persist-plugins/fetch.js.map +1 -1
- package/persist-plugins/fetch.mjs.map +1 -1
- package/persist-plugins/firebase.d.ts +49 -0
- package/persist-plugins/firebase.js +677 -0
- package/persist-plugins/firebase.js.map +1 -0
- package/persist-plugins/firebase.mjs +674 -0
- package/persist-plugins/firebase.mjs.map +1 -0
- package/persist-plugins/query.d.ts +13 -2
- package/persist-plugins/query.js +5 -2
- package/persist-plugins/query.js.map +1 -1
- package/persist-plugins/query.mjs +6 -3
- package/persist-plugins/query.mjs.map +1 -1
- package/persist.js +11 -11
- package/persist.js.map +1 -1
- package/persist.mjs +11 -11
- package/persist.mjs.map +1 -1
- package/src/is.d.ts +1 -0
- package/src/observableInterfaces.d.ts +7 -4
- package/src/persist-plugins/firebase.d.ts +49 -0
- package/src/persist-plugins/query.d.ts +13 -2
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import { observable, observablePrimitive, when, whenReady, isObservable, isObject, isArray, mergeIntoObservable, constructObjectWithPath, isFunction, deconstructObjectWithPath, setAtPath, internal, hasOwnProperty } from '@legendapp/state';
|
|
2
|
+
import { transformPath, transformObject, internal as internal$1 } from '@legendapp/state/persist';
|
|
3
|
+
import { getAuth } from 'firebase/auth';
|
|
4
|
+
import { ref, getDatabase, query, orderByChild, startAt, update, onValue, onChildAdded, onChildChanged, serverTimestamp } from 'firebase/database';
|
|
5
|
+
|
|
6
|
+
const { symbolDelete } = internal;
|
|
7
|
+
const { observablePersistConfiguration } = internal$1;
|
|
8
|
+
function clone(obj) {
|
|
9
|
+
return obj === undefined || obj === null ? obj : JSON.parse(JSON.stringify(obj));
|
|
10
|
+
}
|
|
11
|
+
function getDateModifiedKey(dateModifiedKey) {
|
|
12
|
+
return dateModifiedKey || observablePersistConfiguration.dateModifiedKey || '@';
|
|
13
|
+
}
|
|
14
|
+
const isInitialized = observable(false);
|
|
15
|
+
const symbolSaveValue = Symbol('___obsSaveValue');
|
|
16
|
+
class ObservablePersistFirebaseBase {
|
|
17
|
+
constructor(fns) {
|
|
18
|
+
var _a;
|
|
19
|
+
this._batch = {};
|
|
20
|
+
this._pathsLoadStatus = observable({});
|
|
21
|
+
this.listenErrors = new Map();
|
|
22
|
+
this.saveStates = new Map();
|
|
23
|
+
this.fns = fns;
|
|
24
|
+
this.user = observablePrimitive();
|
|
25
|
+
this.SaveTimeout = (_a = observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.saveTimeout) !== null && _a !== void 0 ? _a : 500;
|
|
26
|
+
when(isInitialized, () => {
|
|
27
|
+
this.fns.onAuthStateChanged((user) => {
|
|
28
|
+
this.user.set(user === null || user === void 0 ? void 0 : user.uid);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async get(params) {
|
|
33
|
+
const { obs, options } = params;
|
|
34
|
+
const { remote } = options;
|
|
35
|
+
if (!remote || !remote.firebase) {
|
|
36
|
+
// If the plugin is set globally but it has no firebase options this plugin can't do anything
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const { requireAuth } = remote;
|
|
40
|
+
let { waitForLoad } = remote;
|
|
41
|
+
if (requireAuth) {
|
|
42
|
+
await whenReady(this.user);
|
|
43
|
+
}
|
|
44
|
+
if (waitForLoad) {
|
|
45
|
+
if (isObservable(waitForLoad)) {
|
|
46
|
+
waitForLoad = whenReady(waitForLoad);
|
|
47
|
+
}
|
|
48
|
+
await waitForLoad;
|
|
49
|
+
}
|
|
50
|
+
const saveState = {
|
|
51
|
+
pendingSaveResults: new Map(),
|
|
52
|
+
pendingSaves: new Map(),
|
|
53
|
+
numSavesPending: 0,
|
|
54
|
+
};
|
|
55
|
+
this.saveStates.set(obs, saveState);
|
|
56
|
+
const { queryByModified } = options.remote.firebase;
|
|
57
|
+
const onLoadParams = {
|
|
58
|
+
waiting: 0,
|
|
59
|
+
onLoad: () => {
|
|
60
|
+
onLoadParams.waiting--;
|
|
61
|
+
if (onLoadParams.waiting === 0) {
|
|
62
|
+
params.onLoad();
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
if (isObject(queryByModified)) {
|
|
67
|
+
// TODO: Track which paths were handled and then afterwards listen to the non-handled ones
|
|
68
|
+
// without modified
|
|
69
|
+
this.iterateListen(obs, params, saveState, [], [], queryByModified, onLoadParams);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const dateModified = queryByModified === true ? params.dateModified : undefined;
|
|
73
|
+
this._listen(obs, params, saveState, [], [], queryByModified, dateModified, onLoadParams);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
iterateListen(obs, params, saveState, path, pathTypes, queryByModified, onLoadParams) {
|
|
77
|
+
const { options } = params;
|
|
78
|
+
const { ignoreKeys } = options.remote.firebase;
|
|
79
|
+
Object.keys(obs).forEach((key) => {
|
|
80
|
+
if (!ignoreKeys || !ignoreKeys.includes(key)) {
|
|
81
|
+
const o = obs[key];
|
|
82
|
+
const q = queryByModified[key] || queryByModified['*'];
|
|
83
|
+
const pathChild = path.concat(key);
|
|
84
|
+
const pathTypesChild = pathTypes.concat(isArray(o.peek()) ? 'array' : 'object');
|
|
85
|
+
let dateModified = undefined;
|
|
86
|
+
if (isObject(q)) {
|
|
87
|
+
this.iterateListen(o, params, saveState, pathChild, pathTypesChild, q, onLoadParams);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
if (q === true || q === '*') {
|
|
91
|
+
dateModified = params.dateModified;
|
|
92
|
+
}
|
|
93
|
+
this._listen(o, params, saveState, pathChild, pathTypesChild, queryByModified, dateModified, onLoadParams);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
retryListens() {
|
|
99
|
+
// If a listen failed but save succeeded, the save should have fixed
|
|
100
|
+
// the permission problem so try again
|
|
101
|
+
this.listenErrors.forEach((listenError) => {
|
|
102
|
+
const { params, path, pathTypes, dateModified, queryByModified, unsubscribes, saveState, onLoadParams } = listenError;
|
|
103
|
+
listenError.retry++;
|
|
104
|
+
if (listenError.retry < 10) {
|
|
105
|
+
unsubscribes.forEach((cb) => cb());
|
|
106
|
+
this._listen(params.obs, params, saveState, path, pathTypes, queryByModified, dateModified, onLoadParams);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.listenErrors.delete(listenError);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async _listen(obs, params, saveState, path, pathTypes, queryByModified, dateModified, onLoadParams) {
|
|
114
|
+
const { options } = params;
|
|
115
|
+
const { once, fieldTransforms, onLoadError, allowSaveIfError, firebase } = options.remote;
|
|
116
|
+
const { syncPath, dateModifiedKey: dateModifiedKeyOption } = firebase;
|
|
117
|
+
let didError = false;
|
|
118
|
+
const dateModifiedKey = getDateModifiedKey(dateModifiedKeyOption);
|
|
119
|
+
const originalPath = path;
|
|
120
|
+
if (fieldTransforms && path.length) {
|
|
121
|
+
path = transformPath(path, pathTypes, fieldTransforms);
|
|
122
|
+
}
|
|
123
|
+
const pathFirebase = syncPath(this.fns.getCurrentUser()) + path.join('/');
|
|
124
|
+
const status$ = this._pathsLoadStatus[pathFirebase].set({
|
|
125
|
+
startedLoading: false,
|
|
126
|
+
isLoaded: false,
|
|
127
|
+
canSave: false,
|
|
128
|
+
});
|
|
129
|
+
let refPath = this.fns.ref(pathFirebase);
|
|
130
|
+
if (dateModified && !isNaN(dateModified)) {
|
|
131
|
+
refPath = this.fns.orderByChild(refPath, dateModifiedKey, dateModified + 1);
|
|
132
|
+
}
|
|
133
|
+
const unsubscribes = [];
|
|
134
|
+
const _onError = (err) => {
|
|
135
|
+
if (!didError) {
|
|
136
|
+
didError = true;
|
|
137
|
+
const existing = this.listenErrors.get(obs);
|
|
138
|
+
if (existing) {
|
|
139
|
+
existing.retry++;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.listenErrors.set(obs, {
|
|
143
|
+
params,
|
|
144
|
+
path: originalPath,
|
|
145
|
+
pathTypes,
|
|
146
|
+
dateModified,
|
|
147
|
+
queryByModified,
|
|
148
|
+
unsubscribes,
|
|
149
|
+
retry: 0,
|
|
150
|
+
saveState,
|
|
151
|
+
onLoadParams,
|
|
152
|
+
});
|
|
153
|
+
params.state.remoteError.set(err);
|
|
154
|
+
onLoadError === null || onLoadError === void 0 ? void 0 : onLoadError(err);
|
|
155
|
+
if (allowSaveIfError) {
|
|
156
|
+
status$.canSave.set(true);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
if (!once) {
|
|
162
|
+
const localState = { changes: {} };
|
|
163
|
+
const cb = this._onChange.bind(this, path, pathTypes, pathFirebase, dateModifiedKey, dateModifiedKeyOption, params, localState, saveState);
|
|
164
|
+
unsubscribes.push(this.fns.onChildAdded(refPath, cb));
|
|
165
|
+
unsubscribes.push(this.fns.onChildChanged(refPath, cb));
|
|
166
|
+
}
|
|
167
|
+
onLoadParams.waiting++;
|
|
168
|
+
unsubscribes.push(this.fns.once(refPath, this._onceValue.bind(this, path, pathTypes, pathFirebase, dateModifiedKey, dateModifiedKeyOption, queryByModified, onLoadParams.onLoad, params), _onError));
|
|
169
|
+
}
|
|
170
|
+
_updatePendingSave(path, value, pending) {
|
|
171
|
+
if (path.length === 0) {
|
|
172
|
+
pending[symbolSaveValue] = value;
|
|
173
|
+
}
|
|
174
|
+
else if (pending[symbolSaveValue]) {
|
|
175
|
+
pending[symbolSaveValue] = mergeIntoObservable(pending[symbolSaveValue], value);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const p = path[0];
|
|
179
|
+
const v = value[p];
|
|
180
|
+
const pendingChild = pending[p];
|
|
181
|
+
// If already have a save info here then don't need to go deeper on the path. Just overwrite the value.
|
|
182
|
+
if (pendingChild && pendingChild[symbolSaveValue] !== undefined) {
|
|
183
|
+
const pendingSaveValue = pendingChild[symbolSaveValue];
|
|
184
|
+
pendingChild[symbolSaveValue] =
|
|
185
|
+
isArray(pendingSaveValue) || isObject(pendingSaveValue)
|
|
186
|
+
? mergeIntoObservable(pendingSaveValue, v)
|
|
187
|
+
: v;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// 1. If nothing here
|
|
191
|
+
// 2. If other strings here
|
|
192
|
+
if (!pending[p]) {
|
|
193
|
+
pending[p] = {};
|
|
194
|
+
}
|
|
195
|
+
if (path.length > 1) {
|
|
196
|
+
this._updatePendingSave(path.slice(1), v, pending[p]);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
pending[p] = { [symbolSaveValue]: v };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async set({ options, path, valueAtPath, pathTypes, obs }) {
|
|
205
|
+
const { remote } = options;
|
|
206
|
+
// If the plugin is set globally but it has no firebase options this plugin can't do anything
|
|
207
|
+
if (!remote || !remote.firebase) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const { requireAuth, waitForSave, saveTimeout, firebase } = remote;
|
|
211
|
+
const { log } = firebase;
|
|
212
|
+
if (requireAuth) {
|
|
213
|
+
await whenReady(this.user);
|
|
214
|
+
}
|
|
215
|
+
if (valueAtPath === undefined) {
|
|
216
|
+
valueAtPath = null;
|
|
217
|
+
}
|
|
218
|
+
const value = constructObjectWithPath(path, clone(valueAtPath), pathTypes);
|
|
219
|
+
const pathCloned = path.slice();
|
|
220
|
+
const syncPath = options.remote.firebase.syncPath(this.fns.getCurrentUser());
|
|
221
|
+
log === null || log === void 0 ? void 0 : log('Saving', value);
|
|
222
|
+
const status$ = this._pathsLoadStatus[syncPath];
|
|
223
|
+
if (!status$.canSave.peek()) {
|
|
224
|
+
// Wait for load
|
|
225
|
+
await when(status$.canSave);
|
|
226
|
+
}
|
|
227
|
+
if (waitForSave) {
|
|
228
|
+
await (isObservable(waitForSave)
|
|
229
|
+
? whenReady(waitForSave)
|
|
230
|
+
: isFunction(waitForSave)
|
|
231
|
+
? waitForSave(value, pathCloned)
|
|
232
|
+
: waitForSave);
|
|
233
|
+
}
|
|
234
|
+
const saveState = this.saveStates.get(obs);
|
|
235
|
+
const { pendingSaveResults, pendingSaves } = saveState;
|
|
236
|
+
if (!pendingSaves.has(syncPath)) {
|
|
237
|
+
pendingSaves.set(syncPath, { options, saves: {} });
|
|
238
|
+
pendingSaveResults.set(syncPath, { saved: [] });
|
|
239
|
+
}
|
|
240
|
+
const pending = pendingSaves.get(syncPath).saves;
|
|
241
|
+
this._updatePendingSave(pathCloned, value, pending);
|
|
242
|
+
if (!saveState.eventSaved) {
|
|
243
|
+
saveState.eventSaved = observablePrimitive();
|
|
244
|
+
}
|
|
245
|
+
// Keep the current eventSaved. This will get reassigned once the timeout activates.
|
|
246
|
+
const eventSaved = saveState.eventSaved;
|
|
247
|
+
const timeout = saveTimeout !== null && saveTimeout !== void 0 ? saveTimeout : this.SaveTimeout;
|
|
248
|
+
if (timeout) {
|
|
249
|
+
if (saveState.timeout) {
|
|
250
|
+
clearTimeout(saveState.timeout);
|
|
251
|
+
}
|
|
252
|
+
saveState.timeout = setTimeout(this._onTimeoutSave.bind(this, saveState), timeout);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
this._onTimeoutSave(saveState);
|
|
256
|
+
}
|
|
257
|
+
const savedOrError = await when(eventSaved);
|
|
258
|
+
if (savedOrError === true) {
|
|
259
|
+
this.retryListens();
|
|
260
|
+
const saveResults = pendingSaveResults.get(syncPath);
|
|
261
|
+
log === null || log === void 0 ? void 0 : log('saved', { value, saves: saveResults === null || saveResults === void 0 ? void 0 : saveResults.saved });
|
|
262
|
+
if (saveResults) {
|
|
263
|
+
const { saved } = saveResults;
|
|
264
|
+
if (saved === null || saved === void 0 ? void 0 : saved.length) {
|
|
265
|
+
// Only want to return from saved one time
|
|
266
|
+
if (saveState.numSavesPending === 0) {
|
|
267
|
+
pendingSaveResults.delete(syncPath);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
saveResults.saved = [];
|
|
271
|
+
}
|
|
272
|
+
let maxModified = 0;
|
|
273
|
+
// Compile a changes object of all the dateModified
|
|
274
|
+
const changes = {};
|
|
275
|
+
for (let i = 0; i < saved.length; i++) {
|
|
276
|
+
const { dateModified, path, dateModifiedKeyOption, dateModifiedKey, value } = saved[i];
|
|
277
|
+
if (dateModified) {
|
|
278
|
+
maxModified = Math.max(dateModified, maxModified);
|
|
279
|
+
if (dateModifiedKeyOption) {
|
|
280
|
+
const deconstructed = deconstructObjectWithPath(path, value);
|
|
281
|
+
// Don't resurrect deleted items
|
|
282
|
+
if (deconstructed !== symbolDelete) {
|
|
283
|
+
Object.assign(changes, constructObjectWithPath(path, {
|
|
284
|
+
[dateModifiedKey]: dateModified,
|
|
285
|
+
}, pathTypes));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
changes,
|
|
292
|
+
dateModified: maxModified || undefined,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
throw savedOrError;
|
|
299
|
+
}
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
_constructBatch(options, batch, basePath, saves, ...path) {
|
|
303
|
+
const { fieldTransforms, firebase } = options.remote;
|
|
304
|
+
const { dateModifiedKey: dateModifiedKeyOption } = firebase;
|
|
305
|
+
const dateModifiedKey = getDateModifiedKey(dateModifiedKeyOption);
|
|
306
|
+
let valSave = saves[symbolSaveValue];
|
|
307
|
+
if (valSave !== undefined) {
|
|
308
|
+
let queryByModified = options.remote.firebase.queryByModified;
|
|
309
|
+
if (queryByModified) {
|
|
310
|
+
if (queryByModified !== true && fieldTransforms) {
|
|
311
|
+
queryByModified = transformObject(queryByModified, fieldTransforms);
|
|
312
|
+
}
|
|
313
|
+
valSave = this.insertDatesToSave(batch, queryByModified, dateModifiedKey, basePath, path, valSave);
|
|
314
|
+
}
|
|
315
|
+
const pathThis = basePath + path.join('/');
|
|
316
|
+
if (pathThis && !batch[pathThis]) {
|
|
317
|
+
batch[pathThis] = valSave;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
Object.keys(saves).forEach((key) => {
|
|
322
|
+
this._constructBatch(options, batch, basePath, saves[key], ...path, key);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
_constructBatchesForSave(pendingSaves) {
|
|
327
|
+
const batches = [];
|
|
328
|
+
pendingSaves.forEach(({ options, saves }) => {
|
|
329
|
+
const basePath = options.remote.firebase.syncPath(this.fns.getCurrentUser());
|
|
330
|
+
const batch = {};
|
|
331
|
+
this._constructBatch(options, batch, basePath, saves);
|
|
332
|
+
batches.push(batch);
|
|
333
|
+
});
|
|
334
|
+
return batches;
|
|
335
|
+
}
|
|
336
|
+
async _onTimeoutSave(saveState) {
|
|
337
|
+
const { pendingSaves, eventSaved } = saveState;
|
|
338
|
+
saveState.timeout = undefined;
|
|
339
|
+
saveState.eventSaved = undefined;
|
|
340
|
+
saveState.numSavesPending++;
|
|
341
|
+
if (pendingSaves.size > 0) {
|
|
342
|
+
const batches = JSON.parse(JSON.stringify(this._constructBatchesForSave(pendingSaves)));
|
|
343
|
+
saveState.savingSaves = pendingSaves;
|
|
344
|
+
// Clear the pendingSaves so that the next batch starts from scratch
|
|
345
|
+
saveState.pendingSaves = new Map();
|
|
346
|
+
if (batches.length > 0) {
|
|
347
|
+
const promises = [];
|
|
348
|
+
for (let i = 0; i < batches.length; i++) {
|
|
349
|
+
const batch = batches[i];
|
|
350
|
+
promises.push(this._saveBatch(batch));
|
|
351
|
+
}
|
|
352
|
+
const results = await Promise.all(promises);
|
|
353
|
+
const errors = results.filter((result) => result.error);
|
|
354
|
+
if (errors.length === 0) {
|
|
355
|
+
saveState.numSavesPending--;
|
|
356
|
+
eventSaved === null || eventSaved === void 0 ? void 0 : eventSaved.set(true);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
eventSaved === null || eventSaved === void 0 ? void 0 : eventSaved.set(errors[0].error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async _saveBatch(batch) {
|
|
365
|
+
const length = JSON.stringify(batch).length;
|
|
366
|
+
let error = undefined;
|
|
367
|
+
// Firebase has a maximum limit of 16MB per save so we constrain our saves to
|
|
368
|
+
// less than 12 to be safe
|
|
369
|
+
if (length > 12e6) {
|
|
370
|
+
const parts = splitLargeObject(batch, 6e6);
|
|
371
|
+
let didSave = true;
|
|
372
|
+
// TODO: Option for logging
|
|
373
|
+
for (let i = 0; i < parts.length; i++) {
|
|
374
|
+
const ret = await this._saveBatch(parts[i]);
|
|
375
|
+
if (ret.error) {
|
|
376
|
+
error = ret.error;
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
didSave = didSave && ret.didSave;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return error ? { error } : { didSave };
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
for (let i = 0; i < 3; i++) {
|
|
386
|
+
try {
|
|
387
|
+
await this.fns.update(batch);
|
|
388
|
+
return { didSave: true };
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
error = err;
|
|
392
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return { error };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
_convertFBTimestamps(obj, dateModifiedKey, dateModifiedKeyOption) {
|
|
399
|
+
let value = obj;
|
|
400
|
+
// Database value can be either { @: number, _: object } or { @: number, ...rest }
|
|
401
|
+
// where @ is the dateModifiedKey
|
|
402
|
+
let dateModified = value[dateModifiedKey];
|
|
403
|
+
if (dateModified) {
|
|
404
|
+
// If user doesn't request a dateModifiedKey then delete it
|
|
405
|
+
if (value._ !== undefined) {
|
|
406
|
+
value = value._;
|
|
407
|
+
}
|
|
408
|
+
else if (dateModified && Object.keys(value).length < 2) {
|
|
409
|
+
value = symbolDelete;
|
|
410
|
+
}
|
|
411
|
+
if (!dateModifiedKeyOption) {
|
|
412
|
+
delete value[dateModifiedKey];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (isObject(value)) {
|
|
416
|
+
Object.keys(value).forEach((k) => {
|
|
417
|
+
const val = value[k];
|
|
418
|
+
if (val !== undefined) {
|
|
419
|
+
if (isObject(val) || isArray(val)) {
|
|
420
|
+
const { value: valueChild, dateModified: dateModifiedChild } = this._convertFBTimestamps(val, dateModifiedKey, dateModifiedKeyOption);
|
|
421
|
+
if (dateModifiedChild) {
|
|
422
|
+
dateModified = Math.max(dateModified || 0, dateModifiedChild);
|
|
423
|
+
}
|
|
424
|
+
if (valueChild !== val) {
|
|
425
|
+
value[k] = valueChild;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return { value, dateModified };
|
|
432
|
+
}
|
|
433
|
+
async _onceValue(path, pathTypes, pathFirebase, dateModifiedKey, dateModifiedKeyOption, queryByModified, onLoad, params, snapshot) {
|
|
434
|
+
const { onChange } = params;
|
|
435
|
+
const outerValue = snapshot.val();
|
|
436
|
+
// If this path previously errored, clear the error state
|
|
437
|
+
const obs = params.obs;
|
|
438
|
+
params.state.remoteError.delete();
|
|
439
|
+
this.listenErrors.delete(obs);
|
|
440
|
+
const status$ = this._pathsLoadStatus[pathFirebase];
|
|
441
|
+
status$.startedLoading.set(true);
|
|
442
|
+
if (outerValue && isObject(outerValue)) {
|
|
443
|
+
let value;
|
|
444
|
+
let dateModified;
|
|
445
|
+
if (queryByModified) {
|
|
446
|
+
const converted = this._convertFBTimestamps(outerValue, dateModifiedKey, dateModifiedKeyOption);
|
|
447
|
+
value = converted.value;
|
|
448
|
+
dateModified = converted.dateModified;
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
value = outerValue;
|
|
452
|
+
}
|
|
453
|
+
value = constructObjectWithPath(path, value, pathTypes);
|
|
454
|
+
const onChangePromise = onChange({
|
|
455
|
+
value,
|
|
456
|
+
path,
|
|
457
|
+
pathTypes,
|
|
458
|
+
mode: queryByModified ? 'assign' : 'set',
|
|
459
|
+
dateModified,
|
|
460
|
+
});
|
|
461
|
+
if (onChangePromise) {
|
|
462
|
+
await onChangePromise;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
onLoad();
|
|
466
|
+
status$.assign({
|
|
467
|
+
canSave: true,
|
|
468
|
+
isLoaded: true,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async _onChange(path, pathTypes, pathFirebase, dateModifiedKey, dateModifiedKeyOption, params, localState, saveState, snapshot) {
|
|
472
|
+
const status$ = this._pathsLoadStatus[pathFirebase];
|
|
473
|
+
const { isLoaded, startedLoading } = status$.peek();
|
|
474
|
+
if (!isLoaded) {
|
|
475
|
+
// If onceValue has not been called yet, then skip onChange because it will come later
|
|
476
|
+
if (!startedLoading)
|
|
477
|
+
return;
|
|
478
|
+
// Wait for load
|
|
479
|
+
await when(status$.isLoaded);
|
|
480
|
+
}
|
|
481
|
+
const { onChange, state } = params;
|
|
482
|
+
// Skip changes if disabled
|
|
483
|
+
if (state.isEnabledRemote.peek() === false)
|
|
484
|
+
return;
|
|
485
|
+
const key = snapshot.key;
|
|
486
|
+
const val = snapshot.val();
|
|
487
|
+
if (val) {
|
|
488
|
+
// eslint-disable-next-line prefer-const
|
|
489
|
+
let { value, dateModified } = this._convertFBTimestamps(val, dateModifiedKey, dateModifiedKeyOption);
|
|
490
|
+
const pathChild = path.concat(key);
|
|
491
|
+
const constructed = constructObjectWithPath(pathChild, value, pathTypes);
|
|
492
|
+
if (!this.addValuesToPendingSaves(pathFirebase, constructed, pathChild, dateModified, dateModifiedKey, dateModifiedKeyOption, saveState, onChange)) {
|
|
493
|
+
localState.changes = setAtPath(localState.changes, pathChild, pathTypes, value);
|
|
494
|
+
// Debounce many child changes into a single onChange
|
|
495
|
+
clearTimeout(localState.timeout);
|
|
496
|
+
localState.timeout = setTimeout(() => {
|
|
497
|
+
const changes = localState.changes;
|
|
498
|
+
localState.changes = {};
|
|
499
|
+
onChange({ value: changes, path, pathTypes, mode: 'assign', dateModified });
|
|
500
|
+
}, 300);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
insertDateToObject(value, dateModifiedKey) {
|
|
505
|
+
const timestamp = this.fns.serverTimestamp();
|
|
506
|
+
if (isObject(value)) {
|
|
507
|
+
return Object.assign(value, {
|
|
508
|
+
[dateModifiedKey]: timestamp,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
return {
|
|
513
|
+
[dateModifiedKey]: timestamp,
|
|
514
|
+
_: value,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
insertDatesToSaveObject(batch, queryByModified, dateModifiedKey, path, value) {
|
|
519
|
+
if (queryByModified === true) {
|
|
520
|
+
value = this.insertDateToObject(value, dateModifiedKey);
|
|
521
|
+
}
|
|
522
|
+
else if (isObject(value)) {
|
|
523
|
+
Object.keys(value).forEach((key) => {
|
|
524
|
+
value[key] = this.insertDatesToSaveObject(batch, queryByModified[key], dateModifiedKey, path + '/' + key, value[key]);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
insertDatesToSave(batch, queryByModified, dateModifiedKey, basePath, path, value) {
|
|
530
|
+
let o = queryByModified;
|
|
531
|
+
for (let i = 0; i < path.length; i++) {
|
|
532
|
+
if (o === true) {
|
|
533
|
+
const pathThis = basePath + path.slice(0, i + 1).join('/');
|
|
534
|
+
if (i === path.length - 1) {
|
|
535
|
+
if (!isObject(value)) {
|
|
536
|
+
return this.insertDateToObject(value, dateModifiedKey);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
if (isObject(value)) {
|
|
540
|
+
value[dateModifiedKey] = this.fns.serverTimestamp();
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
batch[pathThis + '/' + dateModifiedKey] = this.fns.serverTimestamp();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
batch[pathThis + '/' + dateModifiedKey] = this.fns.serverTimestamp();
|
|
549
|
+
}
|
|
550
|
+
return value;
|
|
551
|
+
}
|
|
552
|
+
else if (isObject(o)) {
|
|
553
|
+
o = o[path[i]];
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (o === true && isObject(value)) {
|
|
557
|
+
Object.keys(value).forEach((key) => {
|
|
558
|
+
this.insertDatesToSaveObject(batch, o, dateModifiedKey, basePath + path.join('/') + '/' + key, value[key]);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
else if (o !== undefined) {
|
|
562
|
+
this.insertDatesToSaveObject(batch, o, dateModifiedKey, basePath + path.join('/'), value);
|
|
563
|
+
}
|
|
564
|
+
return value;
|
|
565
|
+
}
|
|
566
|
+
addValuesToPendingSaves(syncPath, value, pathChild, dateModified, dateModifiedKey, dateModifiedKeyOption, saveState, onChange) {
|
|
567
|
+
const { pendingSaveResults, savingSaves } = saveState;
|
|
568
|
+
let found = false;
|
|
569
|
+
const pathArr = syncPath.split('/');
|
|
570
|
+
for (let i = pathArr.length - 1; !found && i >= 0; i--) {
|
|
571
|
+
const p = pathArr[i];
|
|
572
|
+
if (p === '')
|
|
573
|
+
continue;
|
|
574
|
+
const path = pathArr.slice(0, i + 1).join('/') + '/';
|
|
575
|
+
// Look for this saved key in the currently saving saves.
|
|
576
|
+
// If it's being saved locally this must be the remote onChange
|
|
577
|
+
// coming in for this save.
|
|
578
|
+
if (pendingSaveResults.has(path) && (savingSaves === null || savingSaves === void 0 ? void 0 : savingSaves.has(path))) {
|
|
579
|
+
found = true;
|
|
580
|
+
if (pathChild.length > 0) {
|
|
581
|
+
const savingSave = savingSaves.get(path);
|
|
582
|
+
const save = savingSave.saves[pathChild[0]];
|
|
583
|
+
if (!save) {
|
|
584
|
+
found = false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (found) {
|
|
588
|
+
const pending = pendingSaveResults.get(path);
|
|
589
|
+
pending.saved.push({
|
|
590
|
+
value,
|
|
591
|
+
dateModified,
|
|
592
|
+
path: pathChild,
|
|
593
|
+
dateModifiedKey,
|
|
594
|
+
dateModifiedKeyOption,
|
|
595
|
+
onChange,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
value = { [p]: value };
|
|
600
|
+
}
|
|
601
|
+
return found;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function splitLargeObject(obj, limit) {
|
|
605
|
+
const parts = [{}];
|
|
606
|
+
let sizeCount = 0;
|
|
607
|
+
function estimateSize(value) {
|
|
608
|
+
return ('' + value).length + 2; // Convert to string and account for quotes in JSON.
|
|
609
|
+
}
|
|
610
|
+
function recursiveSplit(innerObj, path = []) {
|
|
611
|
+
for (const key in innerObj) {
|
|
612
|
+
if (!hasOwnProperty.call(innerObj, key)) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const newPath = [...path, key];
|
|
616
|
+
const keySize = key.length + 4; // Account for quotes and colon in JSON.
|
|
617
|
+
const val = innerObj[key];
|
|
618
|
+
let itemSize = 0;
|
|
619
|
+
if (val && typeof val === 'object') {
|
|
620
|
+
itemSize = JSON.stringify(val).length;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
itemSize = estimateSize(val);
|
|
624
|
+
}
|
|
625
|
+
if (val && typeof val === 'object' && itemSize > limit) {
|
|
626
|
+
recursiveSplit(val, newPath);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
// Check if the size of the current item exceeds the limit
|
|
630
|
+
if (sizeCount > 0 && sizeCount + keySize + itemSize > limit) {
|
|
631
|
+
parts.push({});
|
|
632
|
+
sizeCount = 0;
|
|
633
|
+
}
|
|
634
|
+
const pathKey = newPath.join('/');
|
|
635
|
+
parts[parts.length - 1][pathKey] = val;
|
|
636
|
+
sizeCount += keySize + itemSize;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
recursiveSplit(obj);
|
|
641
|
+
return parts;
|
|
642
|
+
}
|
|
643
|
+
class ObservablePersistFirebase extends ObservablePersistFirebaseBase {
|
|
644
|
+
constructor() {
|
|
645
|
+
super({
|
|
646
|
+
getCurrentUser: () => { var _a; return (_a = getAuth().currentUser) === null || _a === void 0 ? void 0 : _a.uid; },
|
|
647
|
+
ref: (path) => ref(getDatabase(), path),
|
|
648
|
+
orderByChild: (ref, child, start) => query(ref, orderByChild(child), startAt(start)),
|
|
649
|
+
update: (object) => update(ref(getDatabase()), object),
|
|
650
|
+
once: (ref, callback, callbackError) => {
|
|
651
|
+
let unsubscribe;
|
|
652
|
+
const cb = (snap) => {
|
|
653
|
+
if (unsubscribe) {
|
|
654
|
+
unsubscribe();
|
|
655
|
+
unsubscribe = undefined;
|
|
656
|
+
}
|
|
657
|
+
callback(snap);
|
|
658
|
+
};
|
|
659
|
+
unsubscribe = onValue(ref, cb, callbackError);
|
|
660
|
+
return unsubscribe;
|
|
661
|
+
},
|
|
662
|
+
onChildAdded,
|
|
663
|
+
onChildChanged,
|
|
664
|
+
serverTimestamp,
|
|
665
|
+
onAuthStateChanged: (cb) => getAuth().onAuthStateChanged(cb),
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function initializeLegendFirebase() {
|
|
670
|
+
isInitialized.set(true);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export { ObservablePersistFirebase, initializeLegendFirebase };
|
|
674
|
+
//# sourceMappingURL=firebase.mjs.map
|