[GH-ISSUE #25] Docs for proxy.onMutation((mutationSnapshot, isRemoteChange) and possible issue. #26

Closed
opened 2026-05-23 08:26:26 -06:00 by gitea-mirror · 8 comments
Owner

Originally created by @clibu on GitHub (Mar 22, 2021).
Original GitHub issue: https://github.com/appy-one/acebase/issues/25

Originally assigned to: @appy-one on GitHub.

I was running into an endless recursion issue with an onMutation() callback. I finally worked out that I needed to skip my code in the callback when isRemoteChange is true.

ie. The onMutation() callback was called twice for a single mutation change. Once with isRemoteChange false and again with it true.

I can't find any info on isRemoteChange other than the example code???

A bit more detail. I'd set someProxy.stop = true when it was false. Then I'm setting it to true again which data-proxy.ts code ignores, because the value hasn't changed. I'm simply adding this info in case it is relevant.

Originally created by @clibu on GitHub (Mar 22, 2021). Original GitHub issue: https://github.com/appy-one/acebase/issues/25 Originally assigned to: @appy-one on GitHub. I was running into an endless recursion issue with an ```onMutation()``` callback. I finally worked out that I needed to skip my code in the callback when ``isRemoteChange`` is true. ie. The ```onMutation()``` callback was called twice for a single mutation change. Once with ``isRemoteChange`` ``false`` and again with it ``true``. I can't find any info on ``isRemoteChange`` other than the example code??? A bit more detail. I'd set ``someProxy.stop = true`` when it was ``false``. Then I'm setting it to true again which ``data-proxy.ts`` code ignores, because the value hasn't changed. I'm simply adding this info in case it is relevant.
gitea-mirror 2026-05-23 08:26:26 -06:00
Author
Owner

@appy-one commented on GitHub (Mar 23, 2021):

The isRemoteChange parameter tells you whether the change was made through the proxy itself (isRemoteChange === false), or outside the proxy (isRemoteChange === true), eg through a ref.update(...). If you change something in your mutation callback, you'll get another callback because there's another change.

I'll add this to the documentation. You might want to check out the related onChanged handler on your proxied objects, the jsdoc has this info:

Function that is called each time the value was updated in the database. The callback might be called before the local cache value is updated, so make sure to use the READ-ONLY values passed to your callback. If you make changes to the value being monitored (the proxied version), make sure you are not creating an endless loop!

By the way proxy.stop is a function you would call to destroy the live data proxy, I'm assuming you're referring to a stored value in the proxied object? As in obj.stop = true?

<!-- gh-comment-id:804721875 --> @appy-one commented on GitHub (Mar 23, 2021): The `isRemoteChange` parameter tells you whether the change was made through the proxy itself (`isRemoteChange === false`), or outside the proxy (`isRemoteChange === true`), eg through a `ref.update(...)`. If you change something in your mutation callback, you'll get another callback because there's another change. I'll add this to the documentation. You might want to check out the related `onChanged` handler on your proxied objects, the jsdoc has this info: > Function that is called each time the value was updated in the database. The callback might be called before the local cache value is updated, so make sure to use the READ-ONLY values passed to your callback. If you make changes to the value being monitored (the proxied version), make sure you are not creating an endless loop! By the way `proxy.stop` is a function you would call to destroy the live data proxy, I'm assuming you're referring to a stored value in the proxied object? As in `obj.stop = true`?
Author
Owner

@clibu commented on GitHub (Mar 23, 2021):

Ok, thanks for the extra info. I don't think I'm doing a ref.update() on this property but will check.

The proxy value is being updated in two different places though. The second assignment has the current value, so no change is being done. Could this be causing the 2nd onMutation() callback call with isRemoteChange true.

Yeah stop property name was picked at random. I do use proxy.destroy() which is what you meant here.

<!-- gh-comment-id:804739381 --> @clibu commented on GitHub (Mar 23, 2021): Ok, thanks for the extra info. I don't think I'm doing a ```ref.update()``` on this property but will check. The proxy value is being updated in two different places though. The second assignment has the current value, so no change is being done. Could this be causing the 2nd ``onMutation()`` callback call with ``isRemoteChange`` true. Yeah ``stop`` property name was picked at random. I do use ``proxy.destroy()`` which is what you meant here.
Author
Owner

@clibu commented on GitHub (Mar 23, 2021):

I've had more of a look into why I'm getting a second mutation callback with isRemoteChange = true. I'm definitely not doing any ref.update() and am only doing an assignment to the proxy once.

The second call is coming via storage.js trigger() on line 484 doing ' Subscribe to mutations events on the target path' on line 114 in data-proxy.ts

I don't have any mutation subscriptions that I'm aware off. Further this seems Browser db related as a simple test in Node.js doesn't exhibit the issue.

Maybe it is supposed to behave this way, in which case it needs to documented along with it behaving differently depending on the storage method?

<!-- gh-comment-id:805231243 --> @clibu commented on GitHub (Mar 23, 2021): I've had more of a look into why I'm getting a second mutation callback with ``isRemoteChange = true``. I'm definitely not doing any ``ref.update()`` and am only doing an assignment to the proxy once. The second call is coming via storage.js ``trigger()`` on line 484 doing ' Subscribe to mutations events on the target path' on line 114 in data-proxy.ts I don't have any mutation subscriptions that I'm aware off. Further this seems Browser db related as a simple test in Node.js doesn't exhibit the issue. Maybe it is supposed to behave this way, in which case it needs to documented along with it behaving differently depending on the storage method?
Author
Owner

@appy-one commented on GitHub (Mar 23, 2021):

The quickest way for me to investigate this is if you can you provide sample code I can test with in the browser

<!-- gh-comment-id:805271950 --> @appy-one commented on GitHub (Mar 23, 2021): The quickest way for me to investigate this is if you can you provide sample code I can test with in the browser
Author
Owner

@clibu commented on GitHub (Mar 24, 2021):

Leave it with me. Won't be until later in the week.

<!-- gh-comment-id:805463661 --> @clibu commented on GitHub (Mar 24, 2021): Leave it with me. Won't be until later in the week.
Author
Owner

@appy-one commented on GitHub (Mar 24, 2021):

I've updated the jsdoc description for onMutation, published with acebase-core v1.2.6

<!-- gh-comment-id:805884328 --> @appy-one commented on GitHub (Mar 24, 2021): I've updated the jsdoc description for `onMutation`, published with `acebase-core` v1.2.6
Author
Owner

@clibu commented on GitHub (Mar 25, 2021):

OK, I've tracked down why I'm seeing isRemoteChange = true and two calls to my OnMutation callback and it is most likely my problem.

I have two proxies, one on a parent and one on a child path with the same callback function used for both.

When I change a value in the child path it's proxy OnMutation callback sees it with isRemoteChange = false and the parent proxy OnMutation callback sees it with isRemoteChange = true.

Because I'm using the same callback function for both proxies I didn't realize the two calls were for separate proxies.

Here's a sample that shows the issue, but with two OnMutation handlers.

import { AceBase } from 'acebase';

const db = new AceBase( 'debug' /*,  { logLevel: 'verbose' } */ );

// 
db.ready( async () => {
    const ref1 = db.ref('test'),
          proxy1 = await ref1.proxy( {} ),
          obj1 = proxy1.value;

    // This log's isRemoteChange: true
    proxy1.onMutation( ( mutationSnapshot, isRemoteChange ) => {
        console.log( 'proxy1 - onMutation() path:', mutationSnapshot.ref.path, ', key:', mutationSnapshot.key, ', value:', mutationSnapshot.val(), ', isRemoteChange:', isRemoteChange )
    })

    const ref2 = db.ref('test/songs'),
          proxy2 = await ref2.proxy( {} ),
          obj2 = proxy2.value;
    
    // This log's isRemoteChange: false
    proxy2.onMutation( ( mutationSnapshot, isRemoteChange ) => {
        console.log( 'proxy2 - onMutation() path:', mutationSnapshot.ref.path, ', key:', mutationSnapshot.key, ', value:', mutationSnapshot.val(), ', isRemoteChange:', isRemoteChange )
    })

    setTimeout( () => {
        obj2.showBar = !obj2.showBar
    }, 500 )

});

So is this the behaviour you expect?
And am I wrong using two proxies on the same path?

<!-- gh-comment-id:806435439 --> @clibu commented on GitHub (Mar 25, 2021): OK, I've tracked down why I'm seeing ``isRemoteChange = true`` and two calls to my ``OnMutation`` callback and it is most likely my problem. I have two proxies, one on a parent and one on a child path with the same callback function used for both. When I change a value in the child path it's proxy ``OnMutation`` callback sees it with ``isRemoteChange = false`` and the parent proxy ``OnMutation`` callback sees it with ``isRemoteChange = true``. Because I'm using the same callback function for both proxies I didn't realize the two calls were for separate proxies. Here's a sample that shows the issue, but with two ``OnMutation`` handlers. ```` import { AceBase } from 'acebase'; const db = new AceBase( 'debug' /*, { logLevel: 'verbose' } */ ); // db.ready( async () => { const ref1 = db.ref('test'), proxy1 = await ref1.proxy( {} ), obj1 = proxy1.value; // This log's isRemoteChange: true proxy1.onMutation( ( mutationSnapshot, isRemoteChange ) => { console.log( 'proxy1 - onMutation() path:', mutationSnapshot.ref.path, ', key:', mutationSnapshot.key, ', value:', mutationSnapshot.val(), ', isRemoteChange:', isRemoteChange ) }) const ref2 = db.ref('test/songs'), proxy2 = await ref2.proxy( {} ), obj2 = proxy2.value; // This log's isRemoteChange: false proxy2.onMutation( ( mutationSnapshot, isRemoteChange ) => { console.log( 'proxy2 - onMutation() path:', mutationSnapshot.ref.path, ', key:', mutationSnapshot.key, ', value:', mutationSnapshot.val(), ', isRemoteChange:', isRemoteChange ) }) setTimeout( () => { obj2.showBar = !obj2.showBar }, 500 ) }); ```` So is this the behaviour you expect? And am I wrong using two proxies on the same path?
Author
Owner

@appy-one commented on GitHub (Mar 25, 2021):

Yes, this is expected behaviour. You change the showBar property through proxy2, so in its callback isRemoteChange === false. proxy1 is notified of a change made outside of its knowledge so it fires its callback with isRemoteChange === true.

Why would you want to use 2 proxies on the same path though? The idea behind a proxy is that you can use them as normal objects ("state") and share it throughout your app so you don't have to worry about database updating and synchronization anymore. Instantiate a proxy somewhere in your code once, use it everywhere as if its just local state.

<!-- gh-comment-id:806624365 --> @appy-one commented on GitHub (Mar 25, 2021): Yes, this is expected behaviour. You change the `showBar` property through proxy2, so in its callback `isRemoteChange === false`. proxy1 is notified of a change made outside of its knowledge so it fires its callback with `isRemoteChange === true`. Why would you want to use 2 proxies on the same path though? The idea behind a proxy is that you can use them as normal objects ("state") and share it throughout your app so you don't have to worry about database updating and synchronization anymore. Instantiate a proxy somewhere in your code once, use it everywhere as if its just local state.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: github-starred/acebase#26
No description provided.