[GH-ISSUE #23] Mixing update() etc. with a live data proxy? #21

Closed
opened 2026-05-23 08:25:52 -06:00 by gitea-mirror · 9 comments
Owner

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

Originally assigned to: @appy-one on GitHub.

From what I'm seeing if you call update() on a path that has a Proxy then the proxy isn't aware of the update changes.

Assuming this isn't a bug and is too difficult to resolve that's fine, however the doc's need to boldly point out that you can't mix the two approaches.

Originally created by @clibu on GitHub (Mar 21, 2021). Original GitHub issue: https://github.com/appy-one/acebase/issues/23 Originally assigned to: @appy-one on GitHub. From what I'm seeing if you call ``update()`` on a path that has a Proxy then the proxy isn't aware of the update changes. Assuming this isn't a bug and is too difficult to resolve that's fine, however the doc's need to boldly point out that you can't mix the two approaches.
Author
Owner

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

They should mix perfectly fine, all db changes must update the proxy value

<!-- gh-comment-id:803749838 --> @appy-one commented on GitHub (Mar 22, 2021): They should mix perfectly fine, all db changes must update the proxy value
Author
Owner

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

Good to hear. I'll try and replicate what I was seeing in a small sample app. Might be a day or so.

<!-- gh-comment-id:803758082 --> @clibu commented on GitHub (Mar 22, 2021): Good to hear. I'll try and replicate what I was seeing in a small sample app. Might be a day or so.
Author
Owner

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

Ok my description in the first post wasn't right. The issue is to do with onMutation() callback calls.

I wrote small Node program and tried various things. The issue I am able to reproduce in the Browser, but not in the Node code below is this.

Sample 1 - doesn't show the problem, just describes it

    const ref = db.ref('test');
    const proxy = await ref.proxy( {} );
    const obj = proxy.value;

    proxy.onMutation( ( mutationSnapshot, isRemoteChange ) => {
        console.log( 'onMutation() path:', mutationSnapshot.ref.path, ', key:', mutationSnapshot.key, ', value:', mutationSnapshot.val() )
    })

    await ref.update( { car: 'Jaguar' } )

    // Somewhere else in the app ...
    // Doesn't fire onMutation() callback. If I change the update() call above to obj.car = .. the callback is called.
    obj.car = 'Jaguar'

Sample 2 - delete db if present and run this once. When run again you see behaviour as described.

import { AceBase } from 'acebase';

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

db.ready( async () => {
   const ref = db.ref('test');
    const proxy = await ref.proxy( {} );
    const obj = proxy.value;

    proxy.onMutation( ( mutationSnapshot, isRemoteChange ) => {
        console.log( 'onMutation() path:', mutationSnapshot.ref.path, ', key:', mutationSnapshot.key, ', value:', mutationSnapshot.val() )
    })

    // Run app once then repeat with test1 true a few times. They with false to see onMutation()'s
    const test1 = false
    if ( test1 ){
        // Each run alternates mutations value between 'Porsche' and 'Jaguar'
        await ref.update( { car: 'Jaguar' } )
    }else{
        // mutations always has value 'Porsche' and is only called once, should be twice!
        obj.car = 'Jaguar'
    }

    obj.car = 'Porsche'

});
<!-- gh-comment-id:804600021 --> @clibu commented on GitHub (Mar 23, 2021): Ok my description in the first post wasn't right. The issue is to do with ``onMutation()`` callback calls. I wrote small Node program and tried various things. The issue I am able to reproduce in the Browser, but not in the Node code below is this. Sample 1 - doesn't show the problem, just describes it ```` const ref = db.ref('test'); const proxy = await ref.proxy( {} ); const obj = proxy.value; proxy.onMutation( ( mutationSnapshot, isRemoteChange ) => { console.log( 'onMutation() path:', mutationSnapshot.ref.path, ', key:', mutationSnapshot.key, ', value:', mutationSnapshot.val() ) }) await ref.update( { car: 'Jaguar' } ) // Somewhere else in the app ... // Doesn't fire onMutation() callback. If I change the update() call above to obj.car = .. the callback is called. obj.car = 'Jaguar' ```` Sample 2 - delete db if present and run this once. When run again you see behaviour as described. ```` import { AceBase } from 'acebase'; const db = new AceBase( 'debug' /*, { logLevel: 'verbose' } */ ); db.ready( async () => { const ref = db.ref('test'); const proxy = await ref.proxy( {} ); const obj = proxy.value; proxy.onMutation( ( mutationSnapshot, isRemoteChange ) => { console.log( 'onMutation() path:', mutationSnapshot.ref.path, ', key:', mutationSnapshot.key, ', value:', mutationSnapshot.val() ) }) // Run app once then repeat with test1 true a few times. They with false to see onMutation()'s const test1 = false if ( test1 ){ // Each run alternates mutations value between 'Porsche' and 'Jaguar' await ref.update( { car: 'Jaguar' } ) }else{ // mutations always has value 'Porsche' and is only called once, should be twice! obj.car = 'Jaguar' } obj.car = 'Porsche' }); ````
Author
Owner

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

Small update. In the second example with test1 = false and:

    setTimeout( () => {
        obj.car = 'Porsche'
    }, 0 )  // any delay will do

logs both Jaguar and Porsce.

<!-- gh-comment-id:804686207 --> @clibu commented on GitHub (Mar 23, 2021): Small update. In the second example with ``test1 = false`` and: ```` setTimeout( () => { obj.car = 'Porsche' }, 0 ) // any delay will do ```` logs both Jaguar and Porsce.
Author
Owner

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

Thanks, I'll check!

<!-- gh-comment-id:804722931 --> @appy-one commented on GitHub (Mar 23, 2021): Thanks, I'll check!
Author
Owner

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

It took some time to investigate, but this is a classic race condition going on here!

Simplifying the code, making it easier to reproduce and analyze the sequence of execution:

const ref = db.ref('test');
const proxy = await ref.proxy({});
const obj = proxy.value;

proxy.onMutation((snapshot, isRemote) => {
    console.log(`${snapshot.ref.path} was updated to ${snapshot.val()} through ${isRemote ? 'ref' : 'proxy'}, previous value was: ${snapshot.previous()}`);
});

// Update value through ref:
await ref.update({ car: 'Jaguar' });

// Update value through proxy:
obj.car = 'Porsche';

When the code runs the first time:

  1. value of test/car in database is null (doesn't exist)
  2. proxy values loads, obj.car === undefined
  3. car is updated to 'Jaguar' outside the proxy through ref.update: database is updated, mutation events are prepared and scheduled to fire in next tick.
  4. Setting obj.car = 'Porsche': proxy schedules a database update in next tick.
  5. mutation events for step 3 fire, onMutation callback fires and prints "test/car was updated to Jaguar through ref, previous value was: null"
  6. proxy updates the database async, schedules mutation callbacks to fire in next tick.
  7. mutation events for step 6 fire, onMutation callback fires and prints "test/car was updated to Porsche through proxy, previous value was: Jaguar"

Then, when running the 2nd time:

  1. value of test/car in database is 'Porsche'
  2. proxy value loads, obj.car === 'Porsche'
  3. car is updated to 'Jaguar' outside the proxy through ref.update: database is updated, mutation events are prepared and scheduled to fire in next tick
  4. Setting obj.car = 'Porsche': proxy sees this is the current value (it has not received mutation notification yet) so does not schedule database update.
  5. mutation events for step 3 fire, proxied car value is updated to 'Jaguar', onMutation callback fires and prints "test/car was updated to Jaguar through ref, previous value was: Porsche"

Running 3rd time:

  1. value of test/car in database is 'Jaguar'
  2. proxy value loads, obj.car === 'Jaguar'
  3. car is updated to 'Jaguar' outside the proxy through ref.update: database value does not change so no mutation events are prepared.
  4. Setting obj.car = 'Porsche': proxy schedules a database update in next tick.
  5. mutation events for step 4 fire, onMutation callback fires and prints "test/car was updated to Porsche through proxy, previous value was: Jaguar"

Running again after this will repeat 2nd and 3rd time runs.

In real-world situations this is vitrually impossible to occur, because both updates will have to execute in nearly the same tick, and they both have to 'undo' their respective values. Preventing this will not be possible because of the asynchronous execution of the mutation events. Especially when running over a network connection between an acebase-client and acebase-server.

Long answer short: mystery solved, it's not a bug!

<!-- gh-comment-id:804943933 --> @appy-one commented on GitHub (Mar 23, 2021): It took some time to investigate, but this is a classic race condition going on here! Simplifying the code, making it easier to reproduce and analyze the sequence of execution: ```js const ref = db.ref('test'); const proxy = await ref.proxy({}); const obj = proxy.value; proxy.onMutation((snapshot, isRemote) => { console.log(`${snapshot.ref.path} was updated to ${snapshot.val()} through ${isRemote ? 'ref' : 'proxy'}, previous value was: ${snapshot.previous()}`); }); // Update value through ref: await ref.update({ car: 'Jaguar' }); // Update value through proxy: obj.car = 'Porsche'; ``` When the code runs the first time: 1. value of `test/car` in database is `null` (doesn't exist) 2. proxy values loads, `obj.car === undefined` 3. car is updated to 'Jaguar' outside the proxy through `ref.update`: database is updated, mutation events are prepared and scheduled to fire in next tick. 4. Setting `obj.car = 'Porsche'`: proxy schedules a database update in next tick. 5. mutation events for step 3 fire, onMutation callback fires and prints `"test/car was updated to Jaguar through ref, previous value was: null"` 6. proxy updates the database async, schedules mutation callbacks to fire in next tick. 7. mutation events for step 6 fire, `onMutation` callback fires and prints `"test/car was updated to Porsche through proxy, previous value was: Jaguar"` Then, when running the 2nd time: 1. value of `test/car` in database is `'Porsche'` 2. proxy value loads, `obj.car === 'Porsche'` 3. car is updated to 'Jaguar' outside the proxy through `ref.update`: database is updated, mutation events are prepared and scheduled to fire in next tick 4. Setting `obj.car = 'Porsche'`: proxy sees this is the current value (it has not received mutation notification yet) so does not schedule database update. 5. mutation events for step 3 fire, proxied car value is updated to 'Jaguar', `onMutation` callback fires and prints `"test/car was updated to Jaguar through ref, previous value was: Porsche"` Running 3rd time: 1. value of `test/car` in database is `'Jaguar'` 2. proxy value loads, `obj.car === 'Jaguar'` 3. car is updated to 'Jaguar' outside the proxy through `ref.update`: database value does not change so no mutation events are prepared. 4. Setting `obj.car = 'Porsche'`: proxy schedules a database update in next tick. 5. mutation events for step 4 fire, `onMutation` callback fires and prints `"test/car was updated to Porsche through proxy, previous value was: Jaguar"` Running again after this will repeat 2nd and 3rd time runs. In real-world situations this is vitrually impossible to occur, because both updates will have to execute in nearly the same tick, and they both have to 'undo' their respective values. Preventing this will not be possible because of the asynchronous execution of the mutation events. Especially when running over a network connection between an `acebase-client` and `acebase-server`. Long answer short: mystery solved, it's not a bug!
Author
Owner

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

OK, that all seems ok. I appreciate how complex all of this is.

Does this also explain the test1 = false case without the setTimeout() where only one mutation callback fires?

<!-- gh-comment-id:805223031 --> @clibu commented on GitHub (Mar 23, 2021): OK, that all seems ok. I appreciate how complex all of this is. Does this also explain the ``test1 = false`` case without the ``setTimeout()`` where only one mutation callback fires?
Author
Owner

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

Yes, doing the following will update the db only once because the target value is marked as changed, and the db is updated in the next tick:

obj.car = 'Porsche'; // flags car as changed
obj.car = 'Jaguar'; // Already flagged
// db will be updated once in next tick
<!-- gh-comment-id:805267539 --> @appy-one commented on GitHub (Mar 23, 2021): Yes, doing the following will update the db only once because the target value is marked as changed, and the db is updated in the next tick: ```js obj.car = 'Porsche'; // flags car as changed obj.car = 'Jaguar'; // Already flagged // db will be updated once in next tick ```
Author
Owner

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

Ok, thanks. Maybe a mention in the docs would be prudent. You could reference this issue for more info.😀

<!-- gh-comment-id:805719684 --> @clibu commented on GitHub (Mar 24, 2021): Ok, thanks. Maybe a mention in the docs would be prudent. You could reference this issue for more info.😀
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#21
No description provided.