@legendapp/state 2.2.0-next.74 → 2.2.0-next.76

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 (60) hide show
  1. package/helpers/time.d.ts +2 -2
  2. package/index.d.ts +1 -1
  3. package/index.js +82 -31
  4. package/index.js.map +1 -1
  5. package/index.mjs +81 -32
  6. package/index.mjs.map +1 -1
  7. package/package.json +16 -1
  8. package/persist.js +122 -129
  9. package/persist.js.map +1 -1
  10. package/persist.mjs +122 -129
  11. package/persist.mjs.map +1 -1
  12. package/react.js +5 -5
  13. package/react.js.map +1 -1
  14. package/react.mjs +6 -6
  15. package/react.mjs.map +1 -1
  16. package/src/ObservableObject.ts +34 -15
  17. package/src/batching.ts +9 -3
  18. package/src/computed.ts +4 -2
  19. package/src/globals.ts +17 -7
  20. package/src/helpers.ts +3 -3
  21. package/src/history/undoRedo.ts +111 -0
  22. package/src/is.ts +7 -0
  23. package/src/observableInterfaces.ts +6 -5
  24. package/src/observableTypes.ts +5 -0
  25. package/src/observe.ts +1 -1
  26. package/src/react/For.tsx +6 -6
  27. package/src/sync/activateSyncedNode.ts +9 -25
  28. package/src/sync/syncHelpers.ts +53 -12
  29. package/src/sync/syncObservable.ts +117 -101
  30. package/src/sync-plugins/crud.ts +384 -0
  31. package/src/sync-plugins/fetch.ts +57 -27
  32. package/src/sync-plugins/keel.ts +447 -0
  33. package/src/sync-plugins/supabase.ts +225 -0
  34. package/src/syncTypes.ts +12 -6
  35. package/src/when.ts +6 -1
  36. package/sync-plugins/crud.d.ts +40 -0
  37. package/sync-plugins/crud.js +275 -0
  38. package/sync-plugins/crud.js.map +1 -0
  39. package/sync-plugins/crud.mjs +271 -0
  40. package/sync-plugins/crud.mjs.map +1 -0
  41. package/sync-plugins/fetch.d.ts +8 -7
  42. package/sync-plugins/fetch.js +34 -12
  43. package/sync-plugins/fetch.js.map +1 -1
  44. package/sync-plugins/fetch.mjs +35 -13
  45. package/sync-plugins/fetch.mjs.map +1 -1
  46. package/sync-plugins/keel.d.ts +91 -0
  47. package/sync-plugins/keel.js +278 -0
  48. package/sync-plugins/keel.js.map +1 -0
  49. package/sync-plugins/keel.mjs +274 -0
  50. package/sync-plugins/keel.mjs.map +1 -0
  51. package/sync-plugins/supabase.d.ts +32 -0
  52. package/sync-plugins/supabase.js +134 -0
  53. package/sync-plugins/supabase.js.map +1 -0
  54. package/sync-plugins/supabase.mjs +131 -0
  55. package/sync-plugins/supabase.mjs.map +1 -0
  56. package/sync.d.ts +1 -0
  57. package/sync.js +157 -127
  58. package/sync.js.map +1 -1
  59. package/sync.mjs +156 -129
  60. package/sync.mjs.map +1 -1
@@ -1,8 +1,6 @@
1
1
  import type {
2
- GetMode,
3
2
  NodeValue,
4
3
  Observable,
5
- ObservableOnChangeParams,
6
4
  ObservablePersistState,
7
5
  ObservableSyncFunctions,
8
6
  ObservableSyncGetParams,
@@ -11,7 +9,7 @@ import type {
11
9
  SyncedSetParams,
12
10
  UpdateFn,
13
11
  } from '@legendapp/state';
14
- import { internal, isFunction, isPromise, mergeIntoObservable, when, whenReady } from '@legendapp/state';
12
+ import { internal, isFunction, isPromise, mergeIntoObservable, whenReady } from '@legendapp/state';
15
13
  import { syncObservable } from './syncObservable';
16
14
  const { getProxy, globalState, runWithRetry, symbolLinked, setNodeValue, getNodeValue } = internal;
17
15
 
@@ -20,7 +18,7 @@ export function enableActivateSyncedNode() {
20
18
  const obs$ = getProxy(node);
21
19
  if (node.activationState) {
22
20
  // If it is a Synced
23
- const { get, initial, set, subscribe } = node.activationState!;
21
+ const { get, initial, set } = node.activationState!;
24
22
 
25
23
  let onChange: UpdateFn | undefined = undefined;
26
24
  const pluginRemote: ObservableSyncFunctions = {};
@@ -35,18 +33,21 @@ export function enableActivateSyncedNode() {
35
33
  pluginRemote.get = (params: ObservableSyncGetParams<any>) => {
36
34
  onChange = params.onChange;
37
35
  const updateLastSync = (lastSync: number) => (params.lastSync = lastSync);
38
- const setMode = (mode: GetMode) => (params.mode = mode);
39
36
 
40
37
  const existingValue = getNodeValue(node);
41
38
  const value = runWithRetry(node, { attemptNum: 0 }, () => {
42
- return get!({
39
+ const paramsToGet = {
43
40
  value:
44
41
  isFunction(existingValue) || existingValue?.[symbolLinked] ? undefined : existingValue,
45
42
  lastSync: params.lastSync!,
46
43
  updateLastSync,
47
- setMode,
44
+ mode: params.mode!,
48
45
  refresh,
49
- });
46
+ };
47
+
48
+ const ret = get!(paramsToGet);
49
+ params.mode = paramsToGet.mode;
50
+ return ret;
50
51
  });
51
52
 
52
53
  promiseReturn = value;
@@ -102,23 +103,6 @@ export function enableActivateSyncedNode() {
102
103
  // @ts-expect-error TODO fix these types
103
104
  syncState = syncObservable(obs$, { ...node.activationState, ...pluginRemote });
104
105
 
105
- if (subscribe) {
106
- when(promiseReturn || true, () => {
107
- subscribe({
108
- node,
109
- update: (params: ObservableOnChangeParams) => {
110
- if (!onChange) {
111
- // TODO: Make this message better
112
- console.log('[legend-state] Cannot update immediately before the first return');
113
- } else {
114
- onChange(params);
115
- }
116
- },
117
- refresh,
118
- });
119
- });
120
- }
121
-
122
106
  return { update: onChange!, value: newValue };
123
107
  } else {
124
108
  // If it is not a Synced
@@ -1,15 +1,56 @@
1
- import { isObject } from '@legendapp/state';
2
-
3
- export function removeNullUndefined<T extends Record<string, any>>(val: T) {
4
- if (val) {
5
- Object.keys(val).forEach((key) => {
6
- const v = val[key];
7
- if (v === null || v === undefined) {
8
- delete val[key];
9
- } else if (isObject(v)) {
10
- removeNullUndefined(v);
1
+ import { isDate, isNullOrUndefined, isObject } from '@legendapp/state';
2
+
3
+ export function removeNullUndefined<T extends Record<string, any>>(a: T, recursive?: boolean): T {
4
+ const out: T = {} as T;
5
+ Object.keys(a).forEach((key: keyof T) => {
6
+ if (a[key] !== null && a[key] !== undefined) {
7
+ out[key] = recursive && isObject(a[key]) ? removeNullUndefined(a[key]) : a[key];
8
+ }
9
+ });
10
+
11
+ return out;
12
+ }
13
+
14
+ export function diffObjects<T extends Record<string, any>>(obj1: T, obj2: T, deep: boolean = false): Partial<T> {
15
+ const diff: Partial<T> = {};
16
+ if (!obj1) return obj2 || diff;
17
+ if (!obj2) return obj1 || diff;
18
+
19
+ const keys = new Set<keyof T>([...Object.keys(obj1), ...Object.keys(obj2)] as (keyof T)[]);
20
+
21
+ keys.forEach((key) => {
22
+ const o1 = obj1[key];
23
+ const o2 = obj2[key];
24
+ if (deep ? !deepEqual(o1, o2) : o1 !== o2) {
25
+ if (!isDate(o1) || !isDate(o2) || o1.getTime() !== o2.getTime()) {
26
+ diff[key] = o2;
11
27
  }
12
- });
28
+ }
29
+ });
30
+
31
+ return diff;
32
+ }
33
+ export function deepEqual<T extends Record<string, any> = any>(
34
+ a: T,
35
+ b: T,
36
+ ignoreFields?: string[],
37
+ nullVsUndefined?: boolean,
38
+ ): boolean {
39
+ if (a === b) {
40
+ return true;
41
+ }
42
+ if (isNullOrUndefined(a) !== isNullOrUndefined(b)) {
43
+ return false;
44
+ }
45
+
46
+ if (nullVsUndefined) {
47
+ a = removeNullUndefined(a, /*recursive*/ true);
48
+ b = removeNullUndefined(b, /*recursive*/ true);
13
49
  }
14
- return val;
50
+
51
+ const replacer = ignoreFields
52
+ ? (key: string, value: any) => (ignoreFields.includes(key) ? undefined : value)
53
+ : undefined;
54
+
55
+ return JSON.stringify(a, replacer) === JSON.stringify(b, replacer);
15
56
  }
@@ -5,6 +5,7 @@ import type {
5
5
  NodeValue,
6
6
  Observable,
7
7
  ObservableObject,
8
+ ObservableOnChangeParams,
8
9
  ObservableParam,
9
10
  ObservablePersistPlugin,
10
11
  ObservableSyncClass,
@@ -15,6 +16,7 @@ import type {
15
16
  Synced,
16
17
  SyncedOptions,
17
18
  TypeAtPath,
19
+ UpdateFnParams,
18
20
  } from '@legendapp/state';
19
21
  import {
20
22
  beginBatch,
@@ -538,8 +540,7 @@ async function doChangeLocal(changeInfo: PreppedChangeLocal | undefined) {
538
540
 
539
541
  const persist = syncOptions.persist;
540
542
  const { table, config: configLocal } = parseLocalConfig(persist!);
541
- const configRemote = syncOptions;
542
- const shouldSaveMetadata = persist && configRemote?.offlineBehavior === 'retry';
543
+ const shouldSaveMetadata = persist?.retrySync;
543
544
 
544
545
  if (saveRemote && shouldSaveMetadata) {
545
546
  // First save pending changes before saving local or remote
@@ -574,9 +575,9 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) {
574
575
 
575
576
  const persist = syncOptions.persist;
576
577
  const { table, config: configLocal } = parseLocalConfig(persist!);
577
- const { offlineBehavior, allowSetIfGetError, onBeforeSet, onSetError, waitForSet, onAfterSet } =
578
+ const { allowSetIfGetError, onBeforeSet, onSetError, waitForSet, onAfterSet } =
578
579
  syncOptions || ({} as SyncedOptions);
579
- const shouldSaveMetadata = persist && offlineBehavior === 'retry';
580
+ const shouldSaveMetadata = persist?.retrySync;
580
581
 
581
582
  if (changesRemote.length > 0) {
582
583
  // Wait for remote to be ready before saving
@@ -624,11 +625,11 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) {
624
625
  const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
625
626
  const { changes, lastSync } = saved;
626
627
  if (pathStrs.length > 0) {
628
+ let transformedChanges: object | undefined = undefined;
629
+ const metadata: PersistMetadata = {};
627
630
  if (persist) {
628
- const metadata: PersistMetadata = {};
629
631
  const pendingMetadata = pluginPersist!.getMetadata(table, configLocal)?.pending;
630
632
  const pending = localState.pendingChanges;
631
- let transformedChanges: object | undefined = undefined;
632
633
 
633
634
  for (let i = 0; i < pathStrs.length; i++) {
634
635
  const pathStr = pathStrs[i];
@@ -649,37 +650,39 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) {
649
650
  if (lastSync) {
650
651
  metadata.lastSync = lastSync;
651
652
  }
653
+ }
652
654
 
653
- // Remote can optionally have data that needs to be merged back into the observable,
654
- // for example Firebase may update dateModified with the server timestamp
655
- if (changes && !isEmpty(changes)) {
656
- transformedChanges = transformLoadData(changes, syncOptions, false);
657
- }
655
+ // Remote can optionally have data that needs to be merged back into the observable,
656
+ // for example Firebase may update dateModified with the server timestamp
657
+ if (changes && !isEmpty(changes)) {
658
+ transformedChanges = transformLoadData(changes, syncOptions, false);
659
+ }
658
660
 
659
- if (localState.numSavesOutstanding > 0) {
660
- if (transformedChanges) {
661
- if (!localState.pendingSaveResults) {
662
- localState.pendingSaveResults = [];
663
- }
664
- localState.pendingSaveResults.push(transformedChanges);
661
+ if (localState.numSavesOutstanding > 0) {
662
+ if (transformedChanges) {
663
+ if (!localState.pendingSaveResults) {
664
+ localState.pendingSaveResults = [];
665
665
  }
666
- } else {
667
- let allChanges = [...(localState.pendingSaveResults || []), transformedChanges].filter(
668
- (v) => v !== undefined,
669
- );
670
- if (allChanges.length > 0) {
671
- if (allChanges.some((change) => isPromise(change))) {
672
- allChanges = await Promise.all(allChanges);
673
- }
674
- onChangeRemote(() => mergeIntoObservable(obs, ...allChanges));
666
+ localState.pendingSaveResults.push(transformedChanges);
667
+ }
668
+ } else {
669
+ let allChanges = [...(localState.pendingSaveResults || []), transformedChanges].filter(
670
+ (v) => v !== undefined,
671
+ );
672
+ if (allChanges.length > 0) {
673
+ if (allChanges.some((change) => isPromise(change))) {
674
+ allChanges = await Promise.all(allChanges);
675
675
  }
676
+ onChangeRemote(() => mergeIntoObservable(obs, ...allChanges));
677
+ }
676
678
 
679
+ if (persist) {
677
680
  if (shouldSaveMetadata && !isEmpty(metadata)) {
678
681
  updateMetadata(obs, localState, syncState, syncOptions, metadata);
679
682
  }
680
-
681
- localState.pendingSaveResults = [];
682
683
  }
684
+
685
+ localState.pendingSaveResults = [];
683
686
  }
684
687
  onAfterSet?.();
685
688
  }
@@ -862,6 +865,82 @@ export function syncObservable<T>(
862
865
 
863
866
  if (get) {
864
867
  const runGet = () => {
868
+ const onChange = async ({ value, mode, lastSync }: UpdateFnParams) => {
869
+ mode = mode || syncOptions.mode || 'set';
870
+ if (value !== undefined) {
871
+ value = transformLoadData(value, syncOptions, true);
872
+ if (isPromise(value)) {
873
+ value = await (value as Promise<T>);
874
+ }
875
+
876
+ const pending = localState.pendingChanges;
877
+ const currentValue = obs$.peek();
878
+ if (pending) {
879
+ let didChangeMetadata = false;
880
+ Object.keys(pending).forEach((key) => {
881
+ const p = key.split('/').filter((p) => p !== '');
882
+ const { v, t } = pending[key];
883
+
884
+ if (t.length === 0 || !value) {
885
+ if (isObject(value) && isObject(v)) {
886
+ Object.assign(value, v);
887
+ } else {
888
+ value = v;
889
+ }
890
+ } else if ((value as any)[p[0]] !== undefined) {
891
+ const curValue = getValueAtPath(currentValue as object, p);
892
+ const newValue = getValueAtPath(value as object, p);
893
+ if (JSON.stringify(curValue) === JSON.stringify(newValue)) {
894
+ delete pending[key];
895
+ didChangeMetadata = true;
896
+ } else {
897
+ (value as any) = setAtPath(
898
+ value as any,
899
+ p,
900
+ t,
901
+ v,
902
+ 'merge',
903
+ obs$.peek(),
904
+ (path: string[], value: any) => {
905
+ delete pending[key];
906
+ pending[path.join('/')] = {
907
+ p: null,
908
+ v: value,
909
+ t: t.slice(0, path.length),
910
+ };
911
+ },
912
+ );
913
+ }
914
+ }
915
+ });
916
+
917
+ if (didChangeMetadata) {
918
+ updateMetadata(obs$, localState, syncState, syncOptions, {
919
+ pending,
920
+ });
921
+ }
922
+ }
923
+
924
+ onChangeRemote(() => {
925
+ if (mode === 'assign' && isObject(value)) {
926
+ (obs$ as unknown as Observable<object>).assign(value);
927
+ } else if (mode === 'append' && isArray(value)) {
928
+ (obs$ as unknown as Observable<any[]>).push(...value);
929
+ } else if (mode === 'prepend' && isArray(value)) {
930
+ (obs$ as unknown as Observable<any[]>).splice(0, 0, ...value);
931
+ } else if (mode === 'merge') {
932
+ mergeIntoObservable(obs$, value);
933
+ } else {
934
+ obs$.set(value);
935
+ }
936
+ });
937
+ }
938
+ if (lastSync && syncOptions.persist) {
939
+ updateMetadata(obs$, localState, syncState, syncOptions, {
940
+ lastSync,
941
+ });
942
+ }
943
+ };
865
944
  get({
866
945
  state: syncState,
867
946
  obs: obs$,
@@ -872,87 +951,24 @@ export function syncObservable<T>(
872
951
  syncOptions.onGetError?.(error);
873
952
  },
874
953
  onGet: () => {
954
+ const isFirstLoad = !node.state!.isLoaded.peek();
875
955
  node.state!.assign({
876
956
  isLoaded: true,
877
957
  error: undefined,
878
958
  });
879
- },
880
- onChange: async ({ value, mode, lastSync }) => {
881
- mode = mode || syncOptions.mode || 'set';
882
- if (value !== undefined) {
883
- value = transformLoadData(value, syncOptions, true);
884
- if (isPromise(value)) {
885
- value = await (value as Promise<T>);
886
- }
887
959
 
888
- const pending = localState.pendingChanges;
889
- const currentValue = obs$.peek();
890
- if (pending) {
891
- let didChangeMetadata = false;
892
- Object.keys(pending).forEach((key) => {
893
- const p = key.split('/').filter((p) => p !== '');
894
- const { v, t } = pending[key];
895
-
896
- if (t.length === 0 || !value) {
897
- if (isObject(value) && isObject(v)) {
898
- Object.assign(value, v);
899
- } else {
900
- value = v;
901
- }
902
- } else if ((value as any)[p[0]] !== undefined) {
903
- const curValue = getValueAtPath(currentValue as object, p);
904
- const newValue = getValueAtPath(value as object, p);
905
- if (JSON.stringify(curValue) === JSON.stringify(newValue)) {
906
- delete pending[key];
907
- didChangeMetadata = true;
908
- } else {
909
- (value as any) = setAtPath(
910
- value as any,
911
- p,
912
- t,
913
- v,
914
- 'merge',
915
- obs$.peek(),
916
- (path: string[], value: any) => {
917
- delete pending[key];
918
- pending[path.join('/')] = {
919
- p: null,
920
- v: value,
921
- t: t.slice(0, path.length),
922
- };
923
- },
924
- );
925
- }
926
- }
927
- });
928
-
929
- if (didChangeMetadata) {
930
- updateMetadata(obs$, localState, syncState, syncOptions, {
931
- pending,
932
- });
933
- }
934
- }
935
-
936
- onChangeRemote(() => {
937
- if (mode === 'assign' && isObject(value)) {
938
- (obs$ as unknown as Observable<object>).assign(value);
939
- } else if (mode === 'append' && isArray(value)) {
940
- (obs$ as unknown as Observable<any[]>).push(...value);
941
- } else if (mode === 'prepend' && isArray(value)) {
942
- (obs$ as unknown as Observable<any[]>).splice(0, 0, ...value);
943
- } else if (mode === 'merge') {
944
- mergeIntoObservable(obs$, value);
945
- } else {
946
- obs$.set(value);
947
- }
948
- });
949
- }
950
- if (lastSync && syncOptions.persist) {
951
- updateMetadata(obs$, localState, syncState, syncOptions, {
952
- lastSync,
960
+ if (isFirstLoad && syncOptions.subscribe) {
961
+ syncOptions.subscribe({
962
+ node,
963
+ update: (params: ObservableOnChangeParams) => {
964
+ params.mode ||= syncOptions.mode || 'merge';
965
+ onChange(params);
966
+ },
967
+ refresh: sync,
953
968
  });
954
969
  }
955
970
  },
971
+ onChange,
956
972
  });
957
973
  };
958
974
  runGet();