@legendapp/state 3.0.0-beta.34 → 3.0.0-beta.36

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.js CHANGED
@@ -1637,7 +1637,11 @@ var proxyHandler = {
1637
1637
  return { configurable: false, enumerable: false };
1638
1638
  }
1639
1639
  const value = getNodeValue(node);
1640
- return isPrimitive(value) ? void 0 : Reflect.getOwnPropertyDescriptor(value, prop);
1640
+ if (isPrimitive(value)) {
1641
+ return void 0;
1642
+ }
1643
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, prop);
1644
+ return descriptor ? { ...descriptor, configurable: true } : void 0;
1641
1645
  },
1642
1646
  set(node, prop, value) {
1643
1647
  if (node.isSetting) {
package/index.mjs CHANGED
@@ -1635,7 +1635,11 @@ var proxyHandler = {
1635
1635
  return { configurable: false, enumerable: false };
1636
1636
  }
1637
1637
  const value = getNodeValue(node);
1638
- return isPrimitive(value) ? void 0 : Reflect.getOwnPropertyDescriptor(value, prop);
1638
+ if (isPrimitive(value)) {
1639
+ return void 0;
1640
+ }
1641
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, prop);
1642
+ return descriptor ? { ...descriptor, configurable: true } : void 0;
1639
1643
  },
1640
1644
  set(node, prop, value) {
1641
1645
  if (node.isSetting) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/state",
3
- "version": "3.0.0-beta.34",
3
+ "version": "3.0.0-beta.36",
4
4
  "description": "legend-state",
5
5
  "sideEffects": false,
6
6
  "private": false,
package/react.d.mts CHANGED
@@ -187,4 +187,4 @@ declare function configureReactive({ components, binders, }: {
187
187
  binders?: Record<string, BindKeys>;
188
188
  }): void;
189
189
 
190
- export { type BindKey, type BindKeys, Computed, type ExtractFCPropsType, type FCReactive, type FCReactiveObject, For, type IReactive, Memo, type ObjectShapeWith$, type ReactifyProps, Reactive, type ShapeWith$, type ShapeWithNew$, type ShapeWithPick$, Show, Switch, type UseObserveOptions, type UseSelectorOptions, configureReactive, hasSymbol, observer, reactive, reactiveComponents, reactiveObserver, useSelector as use$, useComputed, useEffectOnce, useIsMounted, useMount, useMountOnce, useObservable, useObservableReducer, useObserve, useObserveEffect, usePauseProvider, useSelector, useUnmount, useUnmountOnce, useWhen, useWhenReady };
190
+ export { type BindKey, type BindKeys, Computed, type ExtractFCPropsType, type FCReactive, type FCReactiveObject, For, type IReactive, Memo, type ObjectShapeWith$, type ReactifyProps, Reactive, type ShapeWith$, type ShapeWithNew$, type ShapeWithPick$, Show, Switch, type UseObserveOptions, type UseSelectorOptions, configureReactive, hasSymbol, observer, reactive, reactiveComponents, reactiveObserver, useSelector as use$, useComputed, useEffectOnce, useIsMounted, useObservable as useLocalObservable, useMount, useMountOnce, useObservable, useObservableReducer, useObserve, useObserveEffect, usePauseProvider, useSelector, useUnmount, useUnmountOnce, useSelector as useValue, useWhen, useWhenReady };
package/react.d.ts CHANGED
@@ -187,4 +187,4 @@ declare function configureReactive({ components, binders, }: {
187
187
  binders?: Record<string, BindKeys>;
188
188
  }): void;
189
189
 
190
- export { type BindKey, type BindKeys, Computed, type ExtractFCPropsType, type FCReactive, type FCReactiveObject, For, type IReactive, Memo, type ObjectShapeWith$, type ReactifyProps, Reactive, type ShapeWith$, type ShapeWithNew$, type ShapeWithPick$, Show, Switch, type UseObserveOptions, type UseSelectorOptions, configureReactive, hasSymbol, observer, reactive, reactiveComponents, reactiveObserver, useSelector as use$, useComputed, useEffectOnce, useIsMounted, useMount, useMountOnce, useObservable, useObservableReducer, useObserve, useObserveEffect, usePauseProvider, useSelector, useUnmount, useUnmountOnce, useWhen, useWhenReady };
190
+ export { type BindKey, type BindKeys, Computed, type ExtractFCPropsType, type FCReactive, type FCReactiveObject, For, type IReactive, Memo, type ObjectShapeWith$, type ReactifyProps, Reactive, type ShapeWith$, type ShapeWithNew$, type ShapeWithPick$, Show, Switch, type UseObserveOptions, type UseSelectorOptions, configureReactive, hasSymbol, observer, reactive, reactiveComponents, reactiveObserver, useSelector as use$, useComputed, useEffectOnce, useIsMounted, useObservable as useLocalObservable, useMount, useMountOnce, useObservable, useObservableReducer, useObserve, useObserveEffect, usePauseProvider, useSelector, useUnmount, useUnmountOnce, useSelector as useValue, useWhen, useWhenReady };
package/react.js CHANGED
@@ -510,9 +510,15 @@ function useIsMounted() {
510
510
  return obs;
511
511
  }
512
512
  function useObservableReducer(reducer, initializerArg, initializer) {
513
- const obs = useObservable(
514
- () => initializerArg !== void 0 && state.isFunction(initializerArg) ? initializer(initializerArg) : initializerArg
515
- );
513
+ const obs = useObservable(() => {
514
+ if (initializer) {
515
+ return initializer(initializerArg);
516
+ }
517
+ if (state.isFunction(initializerArg)) {
518
+ return initializerArg();
519
+ }
520
+ return initializerArg;
521
+ });
516
522
  const dispatch = (action) => {
517
523
  obs.set(reducer(obs.get(), action));
518
524
  };
@@ -622,6 +628,7 @@ exports.use$ = useSelector;
622
628
  exports.useComputed = useComputed;
623
629
  exports.useEffectOnce = useEffectOnce;
624
630
  exports.useIsMounted = useIsMounted;
631
+ exports.useLocalObservable = useObservable;
625
632
  exports.useMount = useMount;
626
633
  exports.useMountOnce = useMountOnce;
627
634
  exports.useObservable = useObservable;
@@ -632,5 +639,6 @@ exports.usePauseProvider = usePauseProvider;
632
639
  exports.useSelector = useSelector;
633
640
  exports.useUnmount = useUnmount;
634
641
  exports.useUnmountOnce = useUnmountOnce;
642
+ exports.useValue = useSelector;
635
643
  exports.useWhen = useWhen;
636
644
  exports.useWhenReady = useWhenReady;
package/react.mjs CHANGED
@@ -504,9 +504,15 @@ function useIsMounted() {
504
504
  return obs;
505
505
  }
506
506
  function useObservableReducer(reducer, initializerArg, initializer) {
507
- const obs = useObservable(
508
- () => initializerArg !== void 0 && isFunction(initializerArg) ? initializer(initializerArg) : initializerArg
509
- );
507
+ const obs = useObservable(() => {
508
+ if (initializer) {
509
+ return initializer(initializerArg);
510
+ }
511
+ if (isFunction(initializerArg)) {
512
+ return initializerArg();
513
+ }
514
+ return initializerArg;
515
+ });
510
516
  const dispatch = (action) => {
511
517
  obs.set(reducer(obs.get(), action));
512
518
  };
@@ -600,4 +606,4 @@ function useWhenReady(predicate, effect) {
600
606
  return useMemo(() => whenReady(predicate, effect), []);
601
607
  }
602
608
 
603
- export { Computed, For, Memo, Reactive, Show, Switch, configureReactive, hasSymbol, observer, reactive, reactiveComponents, reactiveObserver, useSelector as use$, useComputed, useEffectOnce, useIsMounted, useMount, useMountOnce, useObservable, useObservableReducer, useObserve, useObserveEffect, usePauseProvider, useSelector, useUnmount, useUnmountOnce, useWhen, useWhenReady };
609
+ export { Computed, For, Memo, Reactive, Show, Switch, configureReactive, hasSymbol, observer, reactive, reactiveComponents, reactiveObserver, useSelector as use$, useComputed, useEffectOnce, useIsMounted, useObservable as useLocalObservable, useMount, useMountOnce, useObservable, useObservableReducer, useObserve, useObserveEffect, usePauseProvider, useSelector, useUnmount, useUnmountOnce, useSelector as useValue, useWhen, useWhenReady };
@@ -103,6 +103,61 @@ if (process.env.NODE_ENV === "development") {
103
103
  };
104
104
  }
105
105
 
106
+ // src/is.ts
107
+ function isMap(obj) {
108
+ return obj instanceof Map || obj instanceof WeakMap;
109
+ }
110
+ var globalState = {
111
+ pendingNodes: /* @__PURE__ */ new Map(),
112
+ dirtyNodes: /* @__PURE__ */ new Set()
113
+ };
114
+ function replacer(key, value) {
115
+ if (isMap(value)) {
116
+ return {
117
+ __LSType: "Map",
118
+ value: Array.from(value.entries())
119
+ // or with spread: value: [...value]
120
+ };
121
+ } else if (value instanceof Set) {
122
+ return {
123
+ __LSType: "Set",
124
+ value: Array.from(value)
125
+ // or with spread: value: [...value]
126
+ };
127
+ } else if (globalState.replacer) {
128
+ value = globalState.replacer(key, value);
129
+ }
130
+ return value;
131
+ }
132
+ var ISO8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
133
+ function reviver(key, value) {
134
+ if (value) {
135
+ if (typeof value === "string" && ISO8601.test(value)) {
136
+ return new Date(value);
137
+ }
138
+ if (typeof value === "object") {
139
+ if (value.__LSType === "Map") {
140
+ return new Map(value.value);
141
+ } else if (value.__LSType === "Set") {
142
+ return new Set(value.value);
143
+ }
144
+ }
145
+ if (globalState.reviver) {
146
+ value = globalState.reviver(key, value);
147
+ }
148
+ }
149
+ return value;
150
+ }
151
+ function safeStringify(value) {
152
+ return value ? JSON.stringify(value, replacer) : value;
153
+ }
154
+ function safeParse(value) {
155
+ return value ? JSON.parse(value, reviver) : value;
156
+ }
157
+ function clone(value) {
158
+ return safeParse(safeStringify(value));
159
+ }
160
+
106
161
  // src/sync-plugins/firebase.ts
107
162
  var isEnabled$ = state.observable(true);
108
163
  var firebaseConfig = {};
@@ -154,9 +209,6 @@ var fns = {
154
209
  };
155
210
  function syncedFirebase(props) {
156
211
  props = { ...firebaseConfig, ...props };
157
- const saving$ = state.observable({});
158
- const pendingOutgoing$ = state.observable({});
159
- const pendingIncoming$ = state.observable({});
160
212
  let didList = false;
161
213
  const {
162
214
  refPath,
@@ -174,6 +226,102 @@ function syncedFirebase(props) {
174
226
  const { fieldCreatedAt, changesSince } = props;
175
227
  const asType = props.as || "value";
176
228
  const fieldUpdatedAt = props.fieldUpdatedAt || "@";
229
+ const isRealtime = realtime !== false;
230
+ const pendingWrites = /* @__PURE__ */ new Map();
231
+ const enqueuePendingWrite = (key) => {
232
+ let resolveFn;
233
+ let rejectFn;
234
+ const promise = new Promise((resolve, reject) => {
235
+ resolveFn = resolve;
236
+ rejectFn = reject;
237
+ });
238
+ const entry = {
239
+ resolve: resolveFn,
240
+ reject: rejectFn
241
+ };
242
+ const state = pendingWrites.get(key);
243
+ if (state) {
244
+ state.waiting.push(entry);
245
+ state.pendingCount += 1;
246
+ } else {
247
+ pendingWrites.set(key, {
248
+ waiting: [entry],
249
+ ready: [],
250
+ pendingCount: 1
251
+ });
252
+ }
253
+ return { promise, entry };
254
+ };
255
+ const flushPending = (key) => {
256
+ const state = pendingWrites.get(key);
257
+ if (!state) {
258
+ return;
259
+ }
260
+ if (state.pendingCount === 0 && state.staged) {
261
+ const { value, apply } = state.staged;
262
+ state.staged = void 0;
263
+ while (state.ready.length) {
264
+ const entry = state.ready.shift();
265
+ entry.resolve(value);
266
+ }
267
+ if (!state.waiting.length && !state.ready.length) {
268
+ pendingWrites.delete(key);
269
+ }
270
+ apply == null ? void 0 : apply(value);
271
+ }
272
+ };
273
+ const resolvePendingWrite = (key, entry) => {
274
+ const state = pendingWrites.get(key);
275
+ if (!state) {
276
+ return;
277
+ }
278
+ const waitingIndex = state.waiting.indexOf(entry);
279
+ if (waitingIndex >= 0) {
280
+ state.waiting.splice(waitingIndex, 1);
281
+ state.pendingCount = Math.max(0, state.pendingCount - 1);
282
+ }
283
+ state.ready.push(entry);
284
+ flushPending(key);
285
+ };
286
+ const rejectPendingWrite = (key, entry, error) => {
287
+ const state = pendingWrites.get(key);
288
+ if (state) {
289
+ const waitingIndex = state.waiting.indexOf(entry);
290
+ if (waitingIndex >= 0) {
291
+ state.waiting.splice(waitingIndex, 1);
292
+ state.pendingCount = Math.max(0, state.pendingCount - 1);
293
+ } else {
294
+ const readyIndex = state.ready.indexOf(entry);
295
+ if (readyIndex >= 0) {
296
+ state.ready.splice(readyIndex, 1);
297
+ }
298
+ }
299
+ if (!state.waiting.length && !state.ready.length) {
300
+ pendingWrites.delete(key);
301
+ }
302
+ }
303
+ entry.reject(error);
304
+ };
305
+ const handleServerValue = (key, value, apply) => {
306
+ var _a;
307
+ const state = pendingWrites.get(key);
308
+ if (!state || !state.waiting.length && !state.ready.length) {
309
+ pendingWrites.delete(key);
310
+ apply == null ? void 0 : apply(value);
311
+ } else {
312
+ state.staged = {
313
+ value: value && typeof value === "object" ? clone(value) : value,
314
+ apply: apply != null ? apply : (_a = state.staged) == null ? void 0 : _a.apply
315
+ };
316
+ flushPending(key);
317
+ }
318
+ };
319
+ const ensureFieldId = (key, value) => {
320
+ if (fieldId && key && value && typeof value === "object" && !value[fieldId]) {
321
+ value[fieldId] = key;
322
+ }
323
+ return value;
324
+ };
177
325
  const computeRef = (lastSync) => {
178
326
  const pathFirebase = refPath(fns.getCurrentUser());
179
327
  let ref = fns.ref(pathFirebase);
@@ -196,10 +344,7 @@ function syncedFirebase(props) {
196
344
  let values = [];
197
345
  if (!state.isNullOrUndefined(val)) {
198
346
  values = asType === "value" ? [val] : Object.entries(val).map(([key, value]) => {
199
- if (fieldId && !value[fieldId]) {
200
- value[fieldId] = key;
201
- }
202
- return value;
347
+ return ensureFieldId(key, value);
203
348
  });
204
349
  }
205
350
  didList = true;
@@ -209,7 +354,7 @@ function syncedFirebase(props) {
209
354
  );
210
355
  });
211
356
  };
212
- const subscribe = realtime ? ({ lastSync, update: update2, onError }) => {
357
+ const subscribe = isRealtime ? ({ lastSync, update: update2, onError }) => {
213
358
  const ref = computeRef(lastSync);
214
359
  let unsubscribes;
215
360
  if (asType === "value") {
@@ -217,14 +362,12 @@ function syncedFirebase(props) {
217
362
  if (!didList)
218
363
  return;
219
364
  const val = snap.val();
220
- if (saving$[""].get()) {
221
- pendingIncoming$[""].set(val);
222
- } else {
365
+ handleServerValue("", val, (resolvedValue) => {
223
366
  update2({
224
- value: [val],
367
+ value: [resolvedValue],
225
368
  mode: "set"
226
369
  });
227
- }
370
+ });
228
371
  };
229
372
  unsubscribes = [fns.onValue(ref, onValue2, onError)];
230
373
  } else {
@@ -232,31 +375,26 @@ function syncedFirebase(props) {
232
375
  if (!didList)
233
376
  return;
234
377
  const key = snap.key;
235
- const val = snap.val();
236
- if (fieldId && !val[fieldId]) {
237
- val[fieldId] = key;
238
- }
239
- if (saving$[key].get()) {
240
- pendingIncoming$[key].set(val);
241
- } else {
378
+ const val = ensureFieldId(key, snap.val());
379
+ handleServerValue(key, val, (resolvedValue) => {
242
380
  update2({
243
- value: [val],
244
- mode: "assign"
381
+ value: [resolvedValue],
382
+ mode: "merge"
245
383
  });
246
- }
384
+ });
247
385
  };
248
386
  const onChildDelete = (snap) => {
249
387
  if (!didList)
250
388
  return;
251
389
  const key = snap.key;
252
- const val = snap.val();
253
- if (fieldId && !val[fieldId]) {
254
- val[fieldId] = key;
255
- }
256
- val[state.symbolDelete] = true;
257
- update2({
258
- value: [val],
259
- mode: "assign"
390
+ const valueRaw = snap.val();
391
+ const valueWithId = ensureFieldId(key, state.isNullOrUndefined(valueRaw) ? {} : valueRaw);
392
+ valueWithId[state.symbolDelete] = true;
393
+ handleServerValue(key, valueWithId, (resolvedValue) => {
394
+ update2({
395
+ value: [resolvedValue],
396
+ mode: "merge"
397
+ });
260
398
  });
261
399
  };
262
400
  unsubscribes = [
@@ -280,42 +418,52 @@ function syncedFirebase(props) {
280
418
  }
281
419
  return addUpdatedAt(input);
282
420
  };
283
- const upsert = async (input) => {
284
- const id = fieldId ? input[fieldId] : "";
285
- if (saving$[id].get()) {
286
- pendingOutgoing$[id].set(input);
287
- } else {
288
- saving$[id].set(true);
289
- const path = joinPaths(refPath(fns.getCurrentUser()), fieldId ? id : "");
290
- await fns.update(fns.ref(path), input);
291
- saving$[id].set(false);
292
- flushAfterSave();
293
- }
294
- return state.when(
295
- () => !pendingOutgoing$[id].get(),
296
- () => {
297
- const value = pendingIncoming$[id].get();
298
- if (value) {
299
- pendingIncoming$[id].delete();
300
- return value;
301
- }
302
- }
303
- );
304
- };
305
- const flushAfterSave = () => {
306
- const outgoing = pendingOutgoing$.get();
307
- Object.values(outgoing).forEach((value) => {
308
- upsert(value);
421
+ const upsert = (input, params) => {
422
+ const id = fieldId && asType !== "value" ? input[fieldId] : "";
423
+ const pendingKey = fieldId && asType !== "value" ? String(id != null ? id : "") : "";
424
+ const { promise, entry } = enqueuePendingWrite(pendingKey);
425
+ const userId = fns.getCurrentUser();
426
+ const basePath = refPath(userId);
427
+ const childPath = fieldId && asType !== "value" ? pendingKey : "";
428
+ const path = joinPaths(basePath, childPath);
429
+ const ref = fns.ref(path);
430
+ const updatePromise = fns.update(ref, input);
431
+ updatePromise.then(() => {
432
+ resolvePendingWrite(pendingKey, entry);
433
+ }).catch((error) => {
434
+ rejectPendingWrite(pendingKey, entry, error);
309
435
  });
310
- pendingOutgoing$.set({});
436
+ if (!isRealtime) {
437
+ updatePromise.then(() => {
438
+ const onceRef = fieldId && asType !== "value" ? ref : fns.ref(basePath);
439
+ fns.once(
440
+ onceRef,
441
+ (snap) => {
442
+ const rawValue = snap.val();
443
+ const value = fieldId && asType !== "value" ? ensureFieldId(pendingKey, state.isNullOrUndefined(rawValue) ? {} : rawValue) : rawValue;
444
+ handleServerValue(pendingKey, value, (resolvedValue) => {
445
+ params.update({
446
+ value: resolvedValue,
447
+ mode: "merge"
448
+ });
449
+ });
450
+ },
451
+ (error) => {
452
+ rejectPendingWrite(pendingKey, entry, error);
453
+ }
454
+ );
455
+ }).catch(() => {
456
+ });
457
+ }
458
+ return promise;
311
459
  };
312
- const create = readonly ? void 0 : (input) => {
460
+ const create = readonly ? void 0 : (input, params) => {
313
461
  addCreatedAt(input);
314
- return upsert(input);
462
+ return upsert(input, params);
315
463
  };
316
- const update = readonly ? void 0 : (input) => {
464
+ const update = readonly ? void 0 : (input, params) => {
317
465
  addUpdatedAt(input);
318
- return upsert(input);
466
+ return upsert(input, params);
319
467
  };
320
468
  const deleteFn = readonly ? void 0 : (input) => {
321
469
  const path = joinPaths(
@@ -1,4 +1,4 @@
1
- import { observable, symbolDelete, isString, isArray, isObject, computeSelector, isFunction, isNullOrUndefined, isPromise, isNumber, when } from '@legendapp/state';
1
+ import { observable, symbolDelete, isString, isArray, isObject, computeSelector, isFunction, isNullOrUndefined, isPromise, isNumber } from '@legendapp/state';
2
2
  import { syncedCrud } from '@legendapp/state/sync-plugins/crud';
3
3
  import { getAuth } from 'firebase/auth';
4
4
  import { ref, getDatabase, query, orderByChild, startAt, update, onValue, onChildAdded, onChildChanged, onChildRemoved, serverTimestamp, remove, push } from 'firebase/database';
@@ -101,6 +101,61 @@ if (process.env.NODE_ENV === "development") {
101
101
  };
102
102
  }
103
103
 
104
+ // src/is.ts
105
+ function isMap(obj) {
106
+ return obj instanceof Map || obj instanceof WeakMap;
107
+ }
108
+ var globalState = {
109
+ pendingNodes: /* @__PURE__ */ new Map(),
110
+ dirtyNodes: /* @__PURE__ */ new Set()
111
+ };
112
+ function replacer(key, value) {
113
+ if (isMap(value)) {
114
+ return {
115
+ __LSType: "Map",
116
+ value: Array.from(value.entries())
117
+ // or with spread: value: [...value]
118
+ };
119
+ } else if (value instanceof Set) {
120
+ return {
121
+ __LSType: "Set",
122
+ value: Array.from(value)
123
+ // or with spread: value: [...value]
124
+ };
125
+ } else if (globalState.replacer) {
126
+ value = globalState.replacer(key, value);
127
+ }
128
+ return value;
129
+ }
130
+ var ISO8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
131
+ function reviver(key, value) {
132
+ if (value) {
133
+ if (typeof value === "string" && ISO8601.test(value)) {
134
+ return new Date(value);
135
+ }
136
+ if (typeof value === "object") {
137
+ if (value.__LSType === "Map") {
138
+ return new Map(value.value);
139
+ } else if (value.__LSType === "Set") {
140
+ return new Set(value.value);
141
+ }
142
+ }
143
+ if (globalState.reviver) {
144
+ value = globalState.reviver(key, value);
145
+ }
146
+ }
147
+ return value;
148
+ }
149
+ function safeStringify(value) {
150
+ return value ? JSON.stringify(value, replacer) : value;
151
+ }
152
+ function safeParse(value) {
153
+ return value ? JSON.parse(value, reviver) : value;
154
+ }
155
+ function clone(value) {
156
+ return safeParse(safeStringify(value));
157
+ }
158
+
104
159
  // src/sync-plugins/firebase.ts
105
160
  var isEnabled$ = observable(true);
106
161
  var firebaseConfig = {};
@@ -152,9 +207,6 @@ var fns = {
152
207
  };
153
208
  function syncedFirebase(props) {
154
209
  props = { ...firebaseConfig, ...props };
155
- const saving$ = observable({});
156
- const pendingOutgoing$ = observable({});
157
- const pendingIncoming$ = observable({});
158
210
  let didList = false;
159
211
  const {
160
212
  refPath,
@@ -172,6 +224,102 @@ function syncedFirebase(props) {
172
224
  const { fieldCreatedAt, changesSince } = props;
173
225
  const asType = props.as || "value";
174
226
  const fieldUpdatedAt = props.fieldUpdatedAt || "@";
227
+ const isRealtime = realtime !== false;
228
+ const pendingWrites = /* @__PURE__ */ new Map();
229
+ const enqueuePendingWrite = (key) => {
230
+ let resolveFn;
231
+ let rejectFn;
232
+ const promise = new Promise((resolve, reject) => {
233
+ resolveFn = resolve;
234
+ rejectFn = reject;
235
+ });
236
+ const entry = {
237
+ resolve: resolveFn,
238
+ reject: rejectFn
239
+ };
240
+ const state = pendingWrites.get(key);
241
+ if (state) {
242
+ state.waiting.push(entry);
243
+ state.pendingCount += 1;
244
+ } else {
245
+ pendingWrites.set(key, {
246
+ waiting: [entry],
247
+ ready: [],
248
+ pendingCount: 1
249
+ });
250
+ }
251
+ return { promise, entry };
252
+ };
253
+ const flushPending = (key) => {
254
+ const state = pendingWrites.get(key);
255
+ if (!state) {
256
+ return;
257
+ }
258
+ if (state.pendingCount === 0 && state.staged) {
259
+ const { value, apply } = state.staged;
260
+ state.staged = void 0;
261
+ while (state.ready.length) {
262
+ const entry = state.ready.shift();
263
+ entry.resolve(value);
264
+ }
265
+ if (!state.waiting.length && !state.ready.length) {
266
+ pendingWrites.delete(key);
267
+ }
268
+ apply == null ? void 0 : apply(value);
269
+ }
270
+ };
271
+ const resolvePendingWrite = (key, entry) => {
272
+ const state = pendingWrites.get(key);
273
+ if (!state) {
274
+ return;
275
+ }
276
+ const waitingIndex = state.waiting.indexOf(entry);
277
+ if (waitingIndex >= 0) {
278
+ state.waiting.splice(waitingIndex, 1);
279
+ state.pendingCount = Math.max(0, state.pendingCount - 1);
280
+ }
281
+ state.ready.push(entry);
282
+ flushPending(key);
283
+ };
284
+ const rejectPendingWrite = (key, entry, error) => {
285
+ const state = pendingWrites.get(key);
286
+ if (state) {
287
+ const waitingIndex = state.waiting.indexOf(entry);
288
+ if (waitingIndex >= 0) {
289
+ state.waiting.splice(waitingIndex, 1);
290
+ state.pendingCount = Math.max(0, state.pendingCount - 1);
291
+ } else {
292
+ const readyIndex = state.ready.indexOf(entry);
293
+ if (readyIndex >= 0) {
294
+ state.ready.splice(readyIndex, 1);
295
+ }
296
+ }
297
+ if (!state.waiting.length && !state.ready.length) {
298
+ pendingWrites.delete(key);
299
+ }
300
+ }
301
+ entry.reject(error);
302
+ };
303
+ const handleServerValue = (key, value, apply) => {
304
+ var _a;
305
+ const state = pendingWrites.get(key);
306
+ if (!state || !state.waiting.length && !state.ready.length) {
307
+ pendingWrites.delete(key);
308
+ apply == null ? void 0 : apply(value);
309
+ } else {
310
+ state.staged = {
311
+ value: value && typeof value === "object" ? clone(value) : value,
312
+ apply: apply != null ? apply : (_a = state.staged) == null ? void 0 : _a.apply
313
+ };
314
+ flushPending(key);
315
+ }
316
+ };
317
+ const ensureFieldId = (key, value) => {
318
+ if (fieldId && key && value && typeof value === "object" && !value[fieldId]) {
319
+ value[fieldId] = key;
320
+ }
321
+ return value;
322
+ };
175
323
  const computeRef = (lastSync) => {
176
324
  const pathFirebase = refPath(fns.getCurrentUser());
177
325
  let ref = fns.ref(pathFirebase);
@@ -194,10 +342,7 @@ function syncedFirebase(props) {
194
342
  let values = [];
195
343
  if (!isNullOrUndefined(val)) {
196
344
  values = asType === "value" ? [val] : Object.entries(val).map(([key, value]) => {
197
- if (fieldId && !value[fieldId]) {
198
- value[fieldId] = key;
199
- }
200
- return value;
345
+ return ensureFieldId(key, value);
201
346
  });
202
347
  }
203
348
  didList = true;
@@ -207,7 +352,7 @@ function syncedFirebase(props) {
207
352
  );
208
353
  });
209
354
  };
210
- const subscribe = realtime ? ({ lastSync, update: update2, onError }) => {
355
+ const subscribe = isRealtime ? ({ lastSync, update: update2, onError }) => {
211
356
  const ref = computeRef(lastSync);
212
357
  let unsubscribes;
213
358
  if (asType === "value") {
@@ -215,14 +360,12 @@ function syncedFirebase(props) {
215
360
  if (!didList)
216
361
  return;
217
362
  const val = snap.val();
218
- if (saving$[""].get()) {
219
- pendingIncoming$[""].set(val);
220
- } else {
363
+ handleServerValue("", val, (resolvedValue) => {
221
364
  update2({
222
- value: [val],
365
+ value: [resolvedValue],
223
366
  mode: "set"
224
367
  });
225
- }
368
+ });
226
369
  };
227
370
  unsubscribes = [fns.onValue(ref, onValue2, onError)];
228
371
  } else {
@@ -230,31 +373,26 @@ function syncedFirebase(props) {
230
373
  if (!didList)
231
374
  return;
232
375
  const key = snap.key;
233
- const val = snap.val();
234
- if (fieldId && !val[fieldId]) {
235
- val[fieldId] = key;
236
- }
237
- if (saving$[key].get()) {
238
- pendingIncoming$[key].set(val);
239
- } else {
376
+ const val = ensureFieldId(key, snap.val());
377
+ handleServerValue(key, val, (resolvedValue) => {
240
378
  update2({
241
- value: [val],
242
- mode: "assign"
379
+ value: [resolvedValue],
380
+ mode: "merge"
243
381
  });
244
- }
382
+ });
245
383
  };
246
384
  const onChildDelete = (snap) => {
247
385
  if (!didList)
248
386
  return;
249
387
  const key = snap.key;
250
- const val = snap.val();
251
- if (fieldId && !val[fieldId]) {
252
- val[fieldId] = key;
253
- }
254
- val[symbolDelete] = true;
255
- update2({
256
- value: [val],
257
- mode: "assign"
388
+ const valueRaw = snap.val();
389
+ const valueWithId = ensureFieldId(key, isNullOrUndefined(valueRaw) ? {} : valueRaw);
390
+ valueWithId[symbolDelete] = true;
391
+ handleServerValue(key, valueWithId, (resolvedValue) => {
392
+ update2({
393
+ value: [resolvedValue],
394
+ mode: "merge"
395
+ });
258
396
  });
259
397
  };
260
398
  unsubscribes = [
@@ -278,42 +416,52 @@ function syncedFirebase(props) {
278
416
  }
279
417
  return addUpdatedAt(input);
280
418
  };
281
- const upsert = async (input) => {
282
- const id = fieldId ? input[fieldId] : "";
283
- if (saving$[id].get()) {
284
- pendingOutgoing$[id].set(input);
285
- } else {
286
- saving$[id].set(true);
287
- const path = joinPaths(refPath(fns.getCurrentUser()), fieldId ? id : "");
288
- await fns.update(fns.ref(path), input);
289
- saving$[id].set(false);
290
- flushAfterSave();
291
- }
292
- return when(
293
- () => !pendingOutgoing$[id].get(),
294
- () => {
295
- const value = pendingIncoming$[id].get();
296
- if (value) {
297
- pendingIncoming$[id].delete();
298
- return value;
299
- }
300
- }
301
- );
302
- };
303
- const flushAfterSave = () => {
304
- const outgoing = pendingOutgoing$.get();
305
- Object.values(outgoing).forEach((value) => {
306
- upsert(value);
419
+ const upsert = (input, params) => {
420
+ const id = fieldId && asType !== "value" ? input[fieldId] : "";
421
+ const pendingKey = fieldId && asType !== "value" ? String(id != null ? id : "") : "";
422
+ const { promise, entry } = enqueuePendingWrite(pendingKey);
423
+ const userId = fns.getCurrentUser();
424
+ const basePath = refPath(userId);
425
+ const childPath = fieldId && asType !== "value" ? pendingKey : "";
426
+ const path = joinPaths(basePath, childPath);
427
+ const ref = fns.ref(path);
428
+ const updatePromise = fns.update(ref, input);
429
+ updatePromise.then(() => {
430
+ resolvePendingWrite(pendingKey, entry);
431
+ }).catch((error) => {
432
+ rejectPendingWrite(pendingKey, entry, error);
307
433
  });
308
- pendingOutgoing$.set({});
434
+ if (!isRealtime) {
435
+ updatePromise.then(() => {
436
+ const onceRef = fieldId && asType !== "value" ? ref : fns.ref(basePath);
437
+ fns.once(
438
+ onceRef,
439
+ (snap) => {
440
+ const rawValue = snap.val();
441
+ const value = fieldId && asType !== "value" ? ensureFieldId(pendingKey, isNullOrUndefined(rawValue) ? {} : rawValue) : rawValue;
442
+ handleServerValue(pendingKey, value, (resolvedValue) => {
443
+ params.update({
444
+ value: resolvedValue,
445
+ mode: "merge"
446
+ });
447
+ });
448
+ },
449
+ (error) => {
450
+ rejectPendingWrite(pendingKey, entry, error);
451
+ }
452
+ );
453
+ }).catch(() => {
454
+ });
455
+ }
456
+ return promise;
309
457
  };
310
- const create = readonly ? void 0 : (input) => {
458
+ const create = readonly ? void 0 : (input, params) => {
311
459
  addCreatedAt(input);
312
- return upsert(input);
460
+ return upsert(input, params);
313
461
  };
314
- const update = readonly ? void 0 : (input) => {
462
+ const update = readonly ? void 0 : (input, params) => {
315
463
  addUpdatedAt(input);
316
- return upsert(input);
464
+ return upsert(input, params);
317
465
  };
318
466
  const deleteFn = readonly ? void 0 : (input) => {
319
467
  const path = joinPaths(
@@ -52,7 +52,7 @@ function syncedQuery(params) {
52
52
  }
53
53
  return result.data;
54
54
  } else {
55
- observer.refetch();
55
+ return Promise.resolve(observer.refetch()).then((res) => res.data);
56
56
  }
57
57
  };
58
58
  const subscribe = ({ update }) => {
@@ -50,7 +50,7 @@ function syncedQuery(params) {
50
50
  }
51
51
  return result.data;
52
52
  } else {
53
- observer.refetch();
53
+ return Promise.resolve(observer.refetch()).then((res) => res.data);
54
54
  }
55
55
  };
56
56
  const subscribe = ({ update }) => {
package/sync.js CHANGED
@@ -179,28 +179,43 @@ function runWithRetry(state, retryOptions, retryId, fn) {
179
179
  let timeoutRetry;
180
180
  if (mapRetryTimeouts.has(retryId)) {
181
181
  clearTimeout(mapRetryTimeouts.get(retryId));
182
+ mapRetryTimeouts.delete(retryId);
182
183
  }
184
+ const clearRetryState = () => {
185
+ if (timeoutRetry !== void 0) {
186
+ clearTimeout(timeoutRetry);
187
+ timeoutRetry = void 0;
188
+ }
189
+ mapRetryTimeouts.delete(retryId);
190
+ };
183
191
  return new Promise((resolve, reject) => {
184
192
  const run = () => {
185
193
  value.then((val) => {
194
+ state.retryNum = 0;
195
+ clearRetryState();
186
196
  resolve(val);
187
197
  }).catch((error) => {
188
- state.retryNum++;
189
- if (timeoutRetry) {
198
+ if (timeoutRetry !== void 0) {
190
199
  clearTimeout(timeoutRetry);
200
+ timeoutRetry = void 0;
191
201
  }
192
- if (!state.cancelRetry) {
193
- const timeout = createRetryTimeout(retryOptions, state.retryNum, () => {
194
- value = fn(state);
195
- run();
196
- });
197
- if (timeout === false) {
198
- state.cancelRetry = true;
199
- reject(error);
200
- } else {
201
- mapRetryTimeouts.set(retryId, timeout);
202
- timeoutRetry = timeout;
203
- }
202
+ state.retryNum++;
203
+ if (state.cancelRetry) {
204
+ clearRetryState();
205
+ reject(error);
206
+ return;
207
+ }
208
+ const timeout = createRetryTimeout(retryOptions, state.retryNum, () => {
209
+ value = fn(state);
210
+ run();
211
+ });
212
+ if (timeout === false) {
213
+ state.cancelRetry = true;
214
+ clearRetryState();
215
+ reject(error);
216
+ } else {
217
+ timeoutRetry = timeout;
218
+ mapRetryTimeouts.set(retryId, timeout);
204
219
  }
205
220
  });
206
221
  };
@@ -209,6 +224,7 @@ function runWithRetry(state, retryOptions, retryId, fn) {
209
224
  }
210
225
  return value;
211
226
  } catch (error) {
227
+ mapRetryTimeouts.delete(retryId);
212
228
  return Promise.reject(error);
213
229
  }
214
230
  }
@@ -325,9 +341,14 @@ function mergeChanges(changes) {
325
341
  const existing = changesByPath.get(pathStr);
326
342
  if (existing) {
327
343
  if (change.valueAtPath === existing.prevAtPath) {
328
- changesOut.splice(changesOut.indexOf(change), 1);
344
+ const idx = changesOut.indexOf(existing);
345
+ if (idx >= 0) {
346
+ changesOut.splice(idx, 1);
347
+ }
348
+ changesByPath.delete(pathStr);
329
349
  } else {
330
350
  existing.valueAtPath = change.valueAtPath;
351
+ existing.pathTypes = change.pathTypes;
331
352
  }
332
353
  } else {
333
354
  let found = false;
package/sync.mjs CHANGED
@@ -177,28 +177,43 @@ function runWithRetry(state, retryOptions, retryId, fn) {
177
177
  let timeoutRetry;
178
178
  if (mapRetryTimeouts.has(retryId)) {
179
179
  clearTimeout(mapRetryTimeouts.get(retryId));
180
+ mapRetryTimeouts.delete(retryId);
180
181
  }
182
+ const clearRetryState = () => {
183
+ if (timeoutRetry !== void 0) {
184
+ clearTimeout(timeoutRetry);
185
+ timeoutRetry = void 0;
186
+ }
187
+ mapRetryTimeouts.delete(retryId);
188
+ };
181
189
  return new Promise((resolve, reject) => {
182
190
  const run = () => {
183
191
  value.then((val) => {
192
+ state.retryNum = 0;
193
+ clearRetryState();
184
194
  resolve(val);
185
195
  }).catch((error) => {
186
- state.retryNum++;
187
- if (timeoutRetry) {
196
+ if (timeoutRetry !== void 0) {
188
197
  clearTimeout(timeoutRetry);
198
+ timeoutRetry = void 0;
189
199
  }
190
- if (!state.cancelRetry) {
191
- const timeout = createRetryTimeout(retryOptions, state.retryNum, () => {
192
- value = fn(state);
193
- run();
194
- });
195
- if (timeout === false) {
196
- state.cancelRetry = true;
197
- reject(error);
198
- } else {
199
- mapRetryTimeouts.set(retryId, timeout);
200
- timeoutRetry = timeout;
201
- }
200
+ state.retryNum++;
201
+ if (state.cancelRetry) {
202
+ clearRetryState();
203
+ reject(error);
204
+ return;
205
+ }
206
+ const timeout = createRetryTimeout(retryOptions, state.retryNum, () => {
207
+ value = fn(state);
208
+ run();
209
+ });
210
+ if (timeout === false) {
211
+ state.cancelRetry = true;
212
+ clearRetryState();
213
+ reject(error);
214
+ } else {
215
+ timeoutRetry = timeout;
216
+ mapRetryTimeouts.set(retryId, timeout);
202
217
  }
203
218
  });
204
219
  };
@@ -207,6 +222,7 @@ function runWithRetry(state, retryOptions, retryId, fn) {
207
222
  }
208
223
  return value;
209
224
  } catch (error) {
225
+ mapRetryTimeouts.delete(retryId);
210
226
  return Promise.reject(error);
211
227
  }
212
228
  }
@@ -323,9 +339,14 @@ function mergeChanges(changes) {
323
339
  const existing = changesByPath.get(pathStr);
324
340
  if (existing) {
325
341
  if (change.valueAtPath === existing.prevAtPath) {
326
- changesOut.splice(changesOut.indexOf(change), 1);
342
+ const idx = changesOut.indexOf(existing);
343
+ if (idx >= 0) {
344
+ changesOut.splice(idx, 1);
345
+ }
346
+ changesByPath.delete(pathStr);
327
347
  } else {
328
348
  existing.valueAtPath = change.valueAtPath;
349
+ existing.pathTypes = change.pathTypes;
329
350
  }
330
351
  } else {
331
352
  let found = false;