aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.43k stars 2.13k forks source link

Update Model sync error when saving second time #11563

Closed kolodi closed 8 months ago

kolodi commented 1 year ago

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

DataStore

Amplify Categories

No response

Environment information

``` # Put output below this line System: OS: Linux 5.10 Ubuntu 20.04.4 LTS (Focal Fossa) CPU: (12) x64 Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz Memory: 4.21 GB / 7.65 GB Container: Yes Shell: 5.0.17 - /bin/bash Binaries: Node: 18.16.0 - ~/.nvm/versions/node/v18.16.0/bin/node Yarn: 1.22.19 - ~/.nvm/versions/node/v18.16.0/bin/yarn npm: 9.7.1 - ~/.nvm/versions/node/v18.16.0/bin/npm npmPackages: @ampproject/toolbox-optimizer: undefined () @aws-amplify/ui-react: ^5.0.2 => 5.0.2 @aws-amplify/ui-react-internal: undefined () @babel/core: undefined () @babel/runtime: 7.15.4 @edge-runtime/cookies: 3.2.1 @edge-runtime/ponyfill: 2.3.0 @edge-runtime/primitives: 3.0.1 @hapi/accept: undefined () @napi-rs/triples: undefined () @next/font: undefined () @next/react-dev-overlay: undefined () @opentelemetry/api: undefined () @segment/ajv-human-errors: undefined () @types/deep-equal: ^1.0.1 => 1.0.1 @types/node: ^18.16.18 => 18.16.18 @types/react: ^18.2.13 => 18.2.13 @types/react-copy-to-clipboard: ^5.0.4 => 5.0.4 @types/react-currency-format: ^1.0.0 => 1.0.0 @types/react-dom: ^18.2.6 => 18.2.6 @types/react-modal: ^3.16.0 => 3.16.0 @types/react-resizable: ^3.0.4 => 3.0.4 @types/three: ^0.152.1 => 0.152.1 @vercel/nft: undefined () @vercel/og: undefined () acorn: undefined () amphtml-validator: undefined () anser: undefined () arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () autoprefixer: ^10.4.14 => 10.4.14 aws-amplify: ^5.3.1 => 5.3.1 aws-sdk: ^2.1403.0 => 2.1403.0 babel-packages: undefined () browserify-zlib: undefined () browserslist: undefined () buffer: undefined () bytes: undefined () chalk: undefined () ci-info: undefined () cli-select: undefined () client-only: 0.0.1 comment-json: undefined () compression: undefined () conf: undefined () constants-browserify: undefined () content-disposition: undefined () content-type: undefined () cookie: undefined () cross-spawn: undefined () crypto-browserify: undefined () css.escape: undefined () data-uri-to-buffer: undefined () debug: undefined () deep-equal: ^2.2.1 => 2.2.1 devalue: undefined () domain-browser: undefined () dotenv: ^16.3.1 => 16.3.1 edge-runtime: undefined () eslint: ^8.43.0 => 8.43.0 eslint-config-next: ^13.4.7 => 13.4.7 events: undefined () find-cache-dir: undefined () find-up: undefined () fresh: undefined () get-orientation: undefined () glob: ^10.3.0 => undefined (7.1.7, 7.2.3, 10.3.0, , 7.1.6) gzip-size: undefined () http-proxy: undefined () http-proxy-agent: undefined () https-browserify: undefined () https-proxy-agent: undefined () icss-utils: undefined () ignore-loader: undefined () image-size: undefined () is-animated: undefined () is-docker: undefined () is-wsl: undefined () jest-worker: undefined () json5: undefined () jsonwebtoken: undefined () loader-runner: undefined () loader-utils: undefined () local-ssl-proxy: ^2.0.5 => 2.0.5 lodash.curry: undefined () lru-cache: undefined () micromatch: undefined () mini-css-extract-plugin: undefined () nanoid: undefined () native-url: undefined () neo-async: undefined () next: ^13.4.7 => 13.4.7 next-auth: ^4.22.1 => 4.22.1 node-fetch: undefined () node-html-parser: undefined () ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () platform: undefined () postcss: ^8.4.24 => 8.4.24 (8.4.14) postcss-flexbugs-fixes: undefined () postcss-modules-extract-imports: undefined () postcss-modules-local-by-default: undefined () postcss-modules-scope: undefined () postcss-modules-values: undefined () postcss-preset-env: undefined () postcss-safe-parser: undefined () postcss-scss: undefined () postcss-value-parser: undefined () process: undefined () punycode: undefined () qrcode.react: ^3.1.0 => 3.1.0 querystring-es3: undefined () raw-body: undefined () react: ^18.2.0 => 18.2.0 react-builtin: undefined () react-copy-to-clipboard: ^5.1.0 => 5.1.0 react-device-detect: ^2.2.3 => 2.2.3 react-dom: ^18.2.0 => 18.2.0 react-dom-builtin: undefined () react-dom-experimental-builtin: undefined () react-experimental-builtin: undefined () react-icons: ^4.10.1 => 4.10.1 react-is: 18.2.0 react-modal: ^3.16.1 => 3.16.1 react-refresh: 0.12.0 react-resizable: ^3.0.5 => 3.0.5 react-server-dom-webpack-builtin: undefined () react-server-dom-webpack-experimental-builtin: undefined () regenerator-runtime: 0.13.4 sass-loader: undefined () scheduler-builtin: undefined () scheduler-experimental-builtin: undefined () schema-utils: undefined () semver: undefined () send: undefined () server-only: 0.0.1 setimmediate: undefined () shell-quote: undefined () source-map: undefined () stacktrace-parser: undefined () stream-browserify: undefined () stream-http: undefined () string-hash: undefined () string_decoder: undefined () strip-ansi: undefined () tailwindcss: ^3.3.2 => 3.3.2 tar: undefined () terser: undefined () text-table: undefined () three: ^0.153.0 => 0.153.0 timers-browserify: undefined () tty-browserify: undefined () typescript: ^5.1.3 => 5.1.3 ua-parser-js: undefined () undici: undefined () unistore: undefined () usehooks-ts: ^2.9.1 => 2.9.1 util: undefined () vm-browserify: undefined () watchpack: undefined () web-vitals: undefined () webpack: undefined () webpack-sources: undefined () ws: undefined () zod: undefined () zustand: ^4.3.8 => 4.3.8 npmGlobalPackages: @aws-amplify/cli: 12.1.1 corepack: 0.18.1 npm-check-updates: 16.10.12 npm: 9.7.1 ts-node: 10.9.1 ```

Describe the bug

I have an edit form to make changes to a model. First DataStore.query() is used to get the Model data. Here is an example code of saving changes:

// Asset - a datastore model
// original - the previously fetched Asset model by using DataStore.query()
async function saveChanges() {
    const updatedModel = DataStore.save<Asset>(Asset.copyOf(original, updated => {
        updated.ItemName = changes.ItemName;
    }))

    setOriginal(updatedModel) // update stored model (using react useState)
}

It works fine the first time. The second time I try to save new changes, the DataStore.save() itself runs well, but then there is an error during synchronization to the backend. It complains that there are required data missing. I have debugged the sync process and basically, it tries to update the Asset model with only ItemName property in it and there are no other previously existed properties. I have debugged again the DataStore.save() function to see that there are all the necessary properties, thus original still contains all the data. And I think the DataStore.save() would have thrown an error in this case even before the sync process. But it throws nothing. The updatedModel is returned correctly.

Expected behavior

The model should be updated many times without errors not only the first time.

Reproduction steps

  1. Get an item by DataStore.query()
  2. Save the item with DataStore.save(copyof)
  3. Save the item again with DataStore.save(copyof)
  4. Sync error

Code Snippet

// Here is a full code, I made it a bit universal for updating any model
import { DataStore } from "aws-amplify";
import { PersistentModelConstructor, PersistentModel, IdentifierFieldOrIdentifierObject, PersistentModelMetaData } from "@aws-amplify/datastore";
import { useIsMounted } from "usehooks-ts";
import { useEffect, useState } from "react";
import deepEqual from "deep-equal";

export default function useItem<T extends PersistentModel>({
    itemId, itemType
}: {
    itemId: IdentifierFieldOrIdentifierObject<T, PersistentModelMetaData<T>>;
    itemType: PersistentModelConstructor<T>;
}) {

    const isMounted = useIsMounted();
    const [item, setItem] = useState<T | undefined>();
    const [isLoading, setIsLoading] = useState<boolean>(false);
    const [error, setError] = useState<Error | null>(null);
    const [changes, setChanges] = useState<Partial<T>>({} as Partial<T>);

    function setChange({ propertyName, value }: { propertyName: keyof (T), value: any }) {
        const original = item!;
        setChanges(old => {
            const isEqualToOriginal = deepEqual(original[propertyName], value);
            if (isEqualToOriginal) {
                delete old[propertyName];
                console.log("original:", original[propertyName], "\nchanged: ", value, "\nremoved from changes as it is equal to original");
                return { ...old };
            }

            const c = {
                ...old,
                [propertyName]: value
            }
            console.log("original:", original[propertyName], "\nchanged: ", value);
            return c;
        });
    }

    async function saveChanges() {
        const original = item!;
        console.log("save", original, changes);
        const n = await DataStore.save<T>(itemType.copyOf(original, updated => {
            Object.assign(updated, changes);
        }));
        console.log("saved", n);
        setItem(n);
    }

    useEffect(() => {
        setIsLoading(true);
        console.log("fetching item", itemId, itemType.name);

        fetchItem<T>({ itemType, itemId }).then((item) => {
            if (isMounted()) {
                console.log("fetched item", item);
                setItem(item);
                setChanges({});
            }
        }).catch((error) => {
            if (isMounted()) {
                console.log(error);
                setError(error);
            }
        }).finally(() => {
            if (isMounted()) {
                setIsLoading(false);
            }
        });
    }, [itemType, isMounted, itemId]);

    return {
        item,
        changes,
        setChange,
        saveChanges,
        isLoading,
        error,
        hasChanges: Object.keys(changes).length > 0,
    }
}

async function fetchItem<T extends PersistentModel>({
    itemType, itemId
}: {
    itemType: PersistentModelConstructor<T>,
    itemId: IdentifierFieldOrIdentifierObject<T, PersistentModelMetaData<T>>

}) {
    const item = await DataStore.query<T>(itemType, itemId);
    if (!item) {
        throw new Error(`Item ${itemId} of type ${itemType.name} not found`);
    }
    // Temporary delay to simulate network latency 
    // during the first query when the DataStore did yet stored items in local DB
    await new Promise((resolve) => { setTimeout(resolve, 2000) });

    return item;
}

Log output

``` // Here is one of 10 console warning messages from the failed sync process. // The "orgId" is a required property of my "Asset model" so it complains about not being able to resolve conflicts. // As I said, I debugged the process, // and the Asset model contains nothimg besides the property (ItemName) I try to update. // Only the second time. The first update is fine. ConsoleLogger.js:134 [WARN] 34:55.217 DataStore - conflict trycatch Error: Field orgId is required at eval (datastore.js:386:1) at eval (datastore.js:537:1) at Array.forEach () at initializeInstance (datastore.js:534:1) at eval (datastore.js:586:1) at produce (immer.esm.js:1:16032) at new Asset (datastore.js:585:39) at modelInstanceCreator (datastore.js:373:1) at MutationProcessor.defaultConflictHandler [as conflictHandler] (datastore.js:990:1) at MutationProcessor.eval (mutation.js:343:1) at step (tslib.es6.js:102:1) at Object.eval [as throw] (tslib.es6.js:83:46) at rejected (tslib.es6.js:74:42) ```

aws-exports.js

No response

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

svidgen commented 1 year ago

Hey, it looks like you're trying to save updates on top of a record returned from DataStore.save(). Our guidance is to always re-query for the record using DataStore.query() before updating. Can you give that a try and let us know if it works?

kolodi commented 1 year ago

Hey, it looks like you're trying to save updates on top of a record returned from DataStore.save(). Our guidance is to always re-query for the record using DataStore.query() before updating. Can you give that a try and let us know if it works?

Yes, I've tried to do requery the item after update by triggering useEffect with a progressive version. It still was failing, until I have added 1 second delay after saving before refetching again. I wonder of there is a better way to know when i must refetch.


    const [editingVersion, setVersion] = useState<number>(0);

    async function saveChanges() {
        const original = item!;
        console.log("save", original, changes);
        const n = await DataStore.save<T>(itemType.copyOf(original, updated => {
            Object.assign(updated, changes);
        }));
        // this helped
        await new Promise((resolve) => { setTimeout(resolve, 1000) });
        console.log("saved", n);
        setEditingVersion(v => v + 1); // this will trigger useEffect to refetch item
    }

    useEffect(() => {
        setIsLoading(true);
        console.log("fetching item", itemId, itemType.name);

        fetchItem<T>({ itemType, itemId }).then((item) => {
            if (isMounted()) {
                console.log("fetched item", item);
                setItem(item);
                setChanges({});
            }
        }).catch((error) => {
            if (isMounted()) {
                console.log(error);
                setError(error);
            }
        }).finally(() => {
            if (isMounted()) {
                setIsLoading(false);
            }
        });
    }, [itemType, isMounted, itemId, editingVersion]);
cwomack commented 1 year ago

@kolodi, the delay that was introduced may have helped with the time it takes for the mutation to go out (depending on connection speed). If you're looking to be more deterministic for this, you could watch for Hub events like outboxMutationProcessed and introduce more controls over editing and saving data. You could also look for specific models being sent or wait for outbox to be empty (via outboxStatus event) to ensure everything is synched before you allow more edits.

Let me know if that helps you accomplish a better way to know when you need to refetch!

kolodi commented 1 year ago

Hey @cwomack, It seems like the best pattern so far is to use subscription. Initially I thought I might not need them. I have been removing subscriptions from GraphQL schema. But when working with DataStore it is basically assumed that you use them. It stamps warnings when you miss them. But it is not written anywhere in the docs. There should be an official guidelines for the DataStore and the usage of subscription pattern I think, as it is the way to go imo.

Here is an updated generalised code for the useItem I've made

```typescript import { DataStore } from "aws-amplify"; import { PersistentModelConstructor, PersistentModel, IdentifierFieldOrIdentifierObject, PersistentModelMetaData } from "@aws-amplify/datastore"; import { useEffect, useState } from "react"; import deepEqual from "deep-equal"; import { useAbortablePromise } from "../../helpers/useAbortablePromise"; export type UseItemData = ReturnType>; export type WriteablePartial = { -readonly [P in keyof T]?: T[P]; } export default function useItem({ id, itemType }: { id: IdentifierFieldOrIdentifierObject>; itemType: PersistentModelConstructor; }) { const [item, setItem] = useState(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [changes, setChanges] = useState>({} as WriteablePartial); function setChange({ propertyName, value }: { propertyName: keyof (T), value: ValueOf }) { const original = item!; setChanges(old => { const isEqualToOriginal = deepEqual(original[propertyName], value); if (isEqualToOriginal) { delete old[propertyName]; console.log("original:", original[propertyName], "\nchanged: ", value, "\nremoved from changes as it is equal to original"); return { ...old }; } const c = { ...old, [propertyName]: value } console.log("original:", original[propertyName], "\nchanged: ", value); return c; }); } function clearChange(propertyName: keyof (T)) { setChanges(old => { delete old[propertyName]; return { ...old }; }); } function resetChanges() { setChanges({}); } function getInputValue(propertyName: keyof (T)) { // console.log("getInputValue", propertyName, changes[propertyName], item![propertyName]); if(changes[propertyName] === undefined) { return item![propertyName] as T1 | null | undefined; } return changes[propertyName] as T1 | null | undefined; } async function saveChanges() { const original = item!; // console.log("save", original, changes); try { const n = await DataStore.save(itemType.copyOf(original, originalUpdating => { Object.assign(originalUpdating, changes); })); // console.log("saved", n); } catch (error) { console.log(error); setError(new Error("Failed to save changes")); } } async function saveChange(change: WriteablePartial) { const original = item!; try { const n = await DataStore.save(itemType.copyOf(original, originalUpdating => { Object.assign(originalUpdating, change); })); } catch (error) { console.log(error); setError(new Error("Failed to save changes")); } } async function deleteItem(cleanUp?: (item: T) => Promise) { if (!item) return; const original = item; if (cleanUp) { await cleanUp(original); } setIsLoading(true); try { await DataStore.delete(itemType, original); // console.log("deleted", original); } catch (error) { console.log(error); setError(new Error("Failed to delete item")); } finally { setIsLoading(false); } } // TODO: use observableQuery with id as filter instead const { error: fetchingError, isLoading: fetchingLoading } = useAbortablePromise({ promise: DataStore.query(itemType, id), dependencies: [itemType, id], initialDelay: 10, onSuccess: (item) => { // console.log("fetched item", item); setItem(item); setChanges({}); }, onErrror: (error) => { console.log(error); setError(error); } }); function keepChangesIfNotEqualToOriginal(newOriginal: T) { setChanges(old => { const original = newOriginal; const keepEntries = Object.entries(old).filter(([key, value]) => { return !deepEqual(value, original[key]); }); const changesToKeep = Object.fromEntries(keepEntries) as Partial; return changesToKeep; }); } useEffect(() => { const o = DataStore.observe(itemType, id as string).subscribe(msg => { // console.log(msg); // we will have 3 updates if online (1 for local Model update and 2 for remote Model update) if(msg.opType === "DELETE") { setItem(undefined); setError(new Error("Item deleted")); return; } const item = msg.element; setItem(item); keepChangesIfNotEqualToOriginal(item); }); return () => { o.unsubscribe(); } }, [id, itemType]); // console.log("changes", changes); return { item, changes, setChange, clearChange, saveChanges, saveChange, deleteItem, resetChanges, getInputValue, isLoading: fetchingLoading || isLoading, error: fetchingError ?? error, hasChanges: Object.keys(changes).length > 0, } } ```

useAbortablePromise - is just to avoid fetching during multiple remounting during development.

Still this method has issues. Mainly because the observe will trigger at least 3 times when online. And there is no errors coming when the sync with the cloud is failed.

I guess, there is a way to do it by subscribing to the Hub sync messages. And it would be even more complex. Because on one hand you need an updated item and then you need to intercept eventual sync errors, compare if it is related to the item being edited, probably restore the previous item data (as the edit failed, you want the changes to be rolled back and not use locally stored wrong data that can't be synced), and display some meaningful message to the user about the error.

It would be nice to have all the necessary info coming from observe->subscribe message. Besides opType, having things like: sync completed or sync failed, offline/online mode, when sync error occurred provide previous model state. So that you can just update your editing item immediately when in offline, or wait until sync is completed and then update it only once or roll back and display error message to the user.

Hub events then still can be used for the complex offline scenarios, when you need to display sync errors later for the items that are not synced properly once you are back online.

One more thing: It also would be nice to have initial item coming from subscribe. Like first time you subscribe you get back immediately an item even if it is not yet changed, just to avoid additional setup for the first fetch. Like in my code example, I need to fetch the item the first time when mounting the component. But if after subscribing to the observe by id would give you an item at least once, I could just use subscribe and no initial fetch. It is possible when working with lists tho, like so:

Example of list observe pattern

```typescript useEffect(() => { // criteria is for paginating / filtering const o = DataStore.observeQuery(itemType, criteria, { sort: standardSorting, }).subscribe(snapshot => { const { items, isSynced } = snapshot; if (isSynced) { setAllItems(items); } }, error => { setError(error); }); return () => { o.unsubscribe(); } }, [itemType, filters, criteraCreator]); ```

Here I can rely on having the initial list coming from the observe->subscribe rather than fetching it on my own.

kolodi commented 8 months ago

Some final thoughts and useful code snippets

DataStore.queryObserve is not ideal when rendering big data sets, mainly because of 2 reasons:

  1. Pagination is not working as you expect, it basically ignores small limit parameter so it is very different from results obtained from DataStore.query
  2. It causes many rerenders because subscription snapshots arrive multiple times after updates (at least 3 times when online)

If you need a proper pagination I generally use query and manually triggering "refetch" when needed:

import { useQuery } from "@tanstack/react-query";
import { DataStore, PersistentModel, PersistentModelConstructor, ProducerPaginationInput, RecursiveModelPredicateExtender } from "aws-amplify/datastore";
import { useState } from "react";

type ObserveQueryProps<T extends PersistentModel> = {
    modelConstructor: PersistentModelConstructor<T>
    criteria?: RecursiveModelPredicateExtender<T>
    paginationProducer?: ProducerPaginationInput<T>
    criteriaDeps?: any[]
}

/**
 * Query items list from DataStore with working pagination. 
 * Use triggerQuery function to manually trigger refetch.
 * Criteria is a function and it cannot be used as a dependance to trigger refetch,
 * therefore use a separate criteriaDeps, most often the same deps used to recreate criteria
 */
export default function useListQuery<T extends PersistentModel>({
    modelConstructor,
    criteria,
    paginationProducer,
    criteriaDeps = [],
}: ObserveQueryProps<T>) {

    const [v, setV] = useState(0)

    function triggerQuery() {
        setV(p => p + 1)
    }

    const { data: items, isLoading, error } = useQuery({
        queryKey: [modelConstructor, paginationProducer, updateVersion, ...criteriaDeps],
        queryFn: async () => DataStore.query(modelConstructor, criteria, paginationProducer),
        initialData: [],
    })

    return {
        items,
        isLoading,
        error,
        triggerQuery,
    }
}

When working with small lists (can be also filtered with criteria that does not change very often), and when you don't care about pagination and rerenderings, you can DataStore.observeQuery:

import { DataStore, PersistentModel, PersistentModelConstructor, ProducerPaginationInput, RecursiveModelPredicateExtender } from "aws-amplify/datastore";
import { useEffect, useState } from "react";

type ObserveQueryProps<T extends PersistentModel> = {
    modelConstructor: PersistentModelConstructor<T>
    criteria?: RecursiveModelPredicateExtender<T>
    paginationProducer?: ProducerPaginationInput<T>
}

/**
 * Observed query for list items. Note: Pagination is not working properly here. For pagination use useQueryList
 */
export default function useDataStoreListObserved<T extends PersistentModel>({
    modelConstructor,
    criteria,
    paginationProducer,
}: ObserveQueryProps<T>) {

    const [items, setItems] = useState<T[]>([])

    useEffect(() => {
        const sub = DataStore
            .observeQuery(modelConstructor, criteria, paginationProducer)
            .subscribe(({ items }) => {
                setItems(items)
            })

        return () => sub.unsubscribe()
    }, [modelConstructor, criteria, paginationProducer])

    return items
}

For a single item I usually combine 2 approaches:

import { useEffect, useState } from "react"
import { useQuery } from "@tanstack/react-query"
import {
    DataStore,
    IdentifierFieldOrIdentifierObject,
    PersistentModel,
    PersistentModelConstructor,
    PersistentModelMetaData,
} from "aws-amplify/datastore"

export default function useDataStoreItemObserved<T extends PersistentModel>(
    itemType: PersistentModelConstructor<T>,
    id: IdentifierFieldOrIdentifierObject<T, PersistentModelMetaData<T>>,
) {
    const [v, setV] = useState(0)
    const query = useQuery({
        queryKey: [itemType, id, v],
        queryFn: () => DataStore.query(itemType, id),
    })

    useEffect(() => {
        const sub = DataStore
            .observe(itemType, id as string)
            .subscribe(() => {
                setV(vv => vv + 1)
            })

        return () => sub.unsubscribe()
    }, [itemType, id])

    return query
}

Note useQuery here is just to solve annoying async state management issues in react and not for actual network requests.

This issue can be closed i suppose.

The problem that persists for me is that there is no elegant way to avoid unnecessary rerenderings when getting updates in observe and observeQuery. In offline mode you get 1 update, when online you get at least 3. Ideally I'd like to have some sort of flag indicating "final" update for any given operation. For example, when updating an item, if you are in offline mode, you will get am update event with flag "final" set to true soon after local successful update (or error in case there is one), when online, the successful local update should give "final" false, only after successful remote sync you should see the flag to be true. Or, alternately, an "update" status property to know: saved locally but yet to be synced to remote (wait), saved locally but remote sync unavailable (rerender), saved remotely (rerender), error saving locally or remotely (display error). Yes , I really want save/sync errors to be obtained in subscription payload.

chrisbonifacio commented 8 months ago

@kolodi I'll go ahead and close this out. I do want to thank you for taking the time to leave feedback and your findings! We'll take this into consideration for improvements to be made.

If you run into any other issues, please feel free to open a new issue.

chrisbonifacio commented 8 months ago

Also, surfacing save/sync errors through subscriptions would definitely be a feature request. I'm not sure how we could do that however. As far as I understand, the subscriptions are triggered by successful mutations via websocket connection from AppSync rather than our library but perhaps they can be surfaced through the DataStore events instead.

Please also feel free to open a feature request with the details

david-mcafee commented 7 months ago

@kolodi - In response to your concern around re-renders, there is an option to monitor the isSynced status flag to avoid unnecessary re-renders.

Additionally, you are correct that DataStore will return three subscription messages per mutation when online. If you're concerned about the re-renders from those additional messages, you could also add a check to see if the data has changed before processing the payload.

Lastly, you mentioned that it would be helpful to know when a sync has completed, when DataStore is offline/online, etc. I wanted to link to the DataStore events docs if you weren't already aware of them, as DataStore does publish messages for these events.

Pagination is not working as you expect, it basically ignores small limit parameter so it is very different from results obtained from DataStore.query

Are you referring here to adjusting syncPageSize when calling DataStore.configure? I apologize, but I wasn't sure what you meant by it ignoring small limit parameters. If there is any additional guidance we can provide here, please let us know!

Thanks!