Reactivty and prepareDerivedData

From @Wasp
So I'm using prepareDerivedData to, well, derive some data for my items; some of them are tied to each other (ie, an "equipment" type item affects a "skill" type item's bonus). I was hoping that re-calling the derived data methods would cause the TJSDocuments to pick up the alterations, but since it's not a database operation, it doesn't appear to pick that up. Is there a good way allow one item to cause the reactivity of another without manually creating stores?
Moving discusion to forum post.
16 Replies
Magus
Magus•14mo ago
You could create event listeners on the prepareDerivedData, the components could pick up the signals when prepareDerivedData is called and update their local values
TyphonJS (Michael)
TyphonJS (Michael)OP•14mo ago
I definitely think Svelte 5 is going to provide a lot of flexibility in the near future for cases like this. IE all of your derived data can define $state and accessors. Right now there is a circuitous hack to force an update. I don't really want to make the TJSDocument store update mechanism public. I can work out the details of that with a code example. It involves faking a render call to the registered "app" from TJSDocument in the underlying document. The other mechanism is using stores for derived data which is not as horrible as it might sound with the writable-derived library found in TRL. Example code:
import { propertyStore } from '#runtime/svelte/store/writable-derived';

class MyActor extends Actor
{
// For typing consider creating a class that contains the derived data.
#derived = { value1: 0 };

/** @type {ActorStores} */
@stores;

constructor()
{
this.#stores = {
value1: propertyStore(this, 'value1'),
};
}

/**
* Returns derived data stores.
* @returns {ActorStores} Derived data stores.
*/
get stores() { return this.#stores; }

/**
* @returns {number} Value 1
*/
get value1() { return this.#derived.value1; }

/**
* @param {number} newValue New value 1.
*/
set value1(newValue) { this.#stores.value1.set(newValue); }
}

/**
* @typedef ActorStores
*
* @property {import('svelte/store').Writable<number>} Value 1
*/
import { propertyStore } from '#runtime/svelte/store/writable-derived';

class MyActor extends Actor
{
// For typing consider creating a class that contains the derived data.
#derived = { value1: 0 };

/** @type {ActorStores} */
@stores;

constructor()
{
this.#stores = {
value1: propertyStore(this, 'value1'),
};
}

/**
* Returns derived data stores.
* @returns {ActorStores} Derived data stores.
*/
get stores() { return this.#stores; }

/**
* @returns {number} Value 1
*/
get value1() { return this.#derived.value1; }

/**
* @param {number} newValue New value 1.
*/
set value1(newValue) { this.#stores.value1.set(newValue); }
}

/**
* @typedef ActorStores
*
* @property {import('svelte/store').Writable<number>} Value 1
*/
You'll notice if you have been following Svelte 5 and Runes that this setup is very similar except the getters for derived data are not reactive and in your Svelte components you'll have to retrieve the derived store vs usage of accessors. This pattern is nice though because when Svelte 5 is available you can replace the usage of writable-derived with $state.
<script>
import { getContext } from 'svelte';

// 'actor' is a TJSDocument.
const actor = getContext('actor');

// Value 1 is a store.
let value1 = $actor.stores.value1;
</script>

Value 1: {$value1}
<script>
import { getContext } from 'svelte';

// 'actor' is a TJSDocument.
const actor = getContext('actor');

// Value 1 is a store.
let value1 = $actor.stores.value1;
</script>

Value 1: {$value1}
I have tested things out in Svelte 5 and it's less boilerplate, but still a bit circuitous due to $state only working for local variable declarations. The goal of this Svelte 5 demo is to be able to define explicit getters / setters on a class vs dynamically defined for types / documentation and such. Svelte 5 example: https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE32S626DMAyFX8XKJhW2iq5_6UWqtLcY-0HB7dJBghLTbUJ59-VCuWjVKqHi5Hz2ORYdO_EKNUvfOibyGlnKDk3Dlox-GlfoK1aEttayVYU72epC8Yb2mciI141UBB0cCpIKDJyUrGGRrHydXPRiY2VWWEihCXKv2oHAr0BEsb3frsaOYntsiaQAKdKi4sXnroti2O0DmlzzqsW17TArn2ENxvvxiE6hm94bWM0OXmV7rNC4wWHY3sarZclPHEuWkmrRLMdtOHDcx0VPd4HfPn9R5Vr3O-icj4cSFb9iOU2v2pA4KDKqkGDI86gpJ4xe3D7A_obLYNYp-pZRzzz1cOxnOIY-uE5uky3Rz8noPEyK4g4UUqtEf7ABs7zJ9Cjzf9bpaNC_TNXnuUOXa9Y6HFsiAMbZJOOcumdqaQRnCZLBYQh4z949YGI2gP84vcNPfJu_38W7-QXn2MclMQMAAA== I wonder if any advances in the compiler can occur to support explicit accessor definition instead of this middleman / double definition approach. It's less code than the example provided above and removes the annoyance of having to work with stores. The svelte component is nicer though:
<script>
import { getContext } from 'svelte';

// 'actor' is a TJSDocument.
const actor = getContext('actor');
</script>

Value 1: {$actor.value1}
<script>
import { getContext } from 'svelte';

// 'actor' is a TJSDocument.
const actor = getContext('actor');
</script>

Value 1: {$actor.value1}
Wasp
Wasp•14mo ago
Thanks guys, stores ended up being the best way to do it 😄
TyphonJS (Michael)
TyphonJS (Michael)OP•14mo ago
Just remember to stay away from the get function found in svelte/store as it is not performant.
Wasp
Wasp•14mo ago
Hm, I've set it up like you've suggested in Svelte 4, and if I do the following in my items;
console.log(this.#stores.bonus)
this.#stores.bonus.set(this.getBonus());
console.log(this.#stores.bonus)
this.#stores.bonus.set(this.getBonus());
The console.log provides what I expect; {subscribe: Æ’, set: Æ’, update: Æ’}, but the set throws store.subscribe is not a function? ThonkSpin specifically: https://github.com/Haxxer/FoundryVTT-Leobrew/blob/New_Leobrew/src/documents/item/item.js#L52-L53
TypeError: Failed data preparation for Actor.2qJ5YYtsYFtaJRVz.Item.q9dDkAZpdeKhTkPi. store.subscribe is not a function
at subscribe (utils.js:139:22)
at index.js:158:4
at Array.map (<anonymous>)
at index.js:157:38
at Object.subscribe2 [as subscribe] (index.js:82:11)
at update (index.js:68:34)
at Object.set (index.js:91:16)
at set bonus [as bonus] (item.js?t=1698093657038:32:22)
at LeobrewItem._prepareDerivedBonuses (item.js?t=1698093657038:52:15)
at LeobrewItem.prepareDerivedData (item.js?t=1698093657038:46:8)
TypeError: Failed data preparation for Actor.2qJ5YYtsYFtaJRVz.Item.q9dDkAZpdeKhTkPi. store.subscribe is not a function
at subscribe (utils.js:139:22)
at index.js:158:4
at Array.map (<anonymous>)
at index.js:157:38
at Object.subscribe2 [as subscribe] (index.js:82:11)
at update (index.js:68:34)
at Object.set (index.js:91:16)
at set bonus [as bonus] (item.js?t=1698093657038:32:22)
at LeobrewItem._prepareDerivedBonuses (item.js?t=1698093657038:52:15)
at LeobrewItem.prepareDerivedData (item.js?t=1698093657038:46:8)
TyphonJS (Michael)
TyphonJS (Michael)OP•14mo ago
I did post somewhat pseudocode which could be the problem. I was basing it on TJSPosition: https://github.com/typhonjs-svelte/runtime-base/blob/main/src/svelte/store/position/TJSPosition.js. This is a complex custom store w/ children stores, so it may not be the easiest to follow. It is a pattern that I use in several other areas though. Perhaps run your code w/ a production build and grab the stack trace from the browser which should point to the files / source in a likely more clear stack trace. In the stack trace above all of the references to index.js I assume is the bundled output. The at Array.map (<anonymous>) line is somewhat suspect, but not sure where it is located presently. I'll also come up with a Svelte REPL example that can be confirmed and show this general pattern independently.
Wasp
Wasp•14mo ago
Hm, it seems that when I set the propertyStores, the underlying subscription method treats the item itself as a store? I'm a bit confused, heh
No description
No description
Wasp
Wasp•14mo ago
I suppose that my item class would need a subscribe method, lest it won't know how to handle it
TyphonJS (Michael)
TyphonJS (Michael)OP•14mo ago
I don't believe you can pass multiple callbacks ala ...callbacks though that is probably not the issue you are tracking down.
Wasp
Wasp•14mo ago
I... am not I am simply calling set() on the propertyStore defined as propertyStore(this, "bonus"), like your example Once it is set, that underlying svelte method is run, treating the item itself as the store, for some reason. I'm not calling subscribe() myself
TyphonJS (Michael)
TyphonJS (Michael)OP•14mo ago
Oops... Yeah... Re the propertyStore thing... Definitely brain farted there. TJSPosition which I was grabbing the pseudocode for is a store w/ the child stores. You can still use this approach though and I do in other areas slightly differently. Check out SvelteReactive: https://github.com/typhonjs-fvtt-lib/svelte/blob/main/src/application/internal/state-reactive/SvelteReactive.js#L576-L604 For the actor / items you'd change things to:
constructor(...args) {
super(...args);

const derivedWritable = writable(this.#derived);
this.#stores = {
bonus: propertyStore(derivedWritable, "bonus"),
subSkills: propertyStore(derivedWritable, "subSkills"),
};
}
constructor(...args) {
super(...args);

const derivedWritable = writable(this.#derived);
this.#stores = {
bonus: propertyStore(derivedWritable, "bonus"),
subSkills: propertyStore(derivedWritable, "subSkills"),
};
}
This general approach allows you to store the data in an organized manner / efficient access for the getter accessors and still benefit from creating the stores for reactive access.
Wasp
Wasp•14mo ago
Yeah, I delved into the TJSPosition and saw the discrepancies, hence my confusion! Thanks for clearing that up 😄 Ayo, great success!
TyphonJS (Michael)
TyphonJS (Michael)OP•14mo ago
Awesome... So everything is working well I take it. 😄 The nice thing about the getter for stores is that you can use destructuring to access them in one statement.
Wasp
Wasp•14mo ago
chefskiss
Wasp
Wasp•14mo ago
I lurv myself TJSDocument & reactive stores; setting up a chain of sheet rerenders just to get the skill list to refresh when external documents change would have been a nightmare
TyphonJS (Michael)
TyphonJS (Michael)OP•14mo ago
Right on... I can't wait to get the other half of TJSDocument complete especially w/ the Svelte 5 angles to it. Just have to try and convince kgar to give things a look. I'm making a new TRL release soon that thoroughly handles PopOut / separate browser window for existing components / focus management / browser access (clipboard / eye dropper) w/ reactive monitoring of the active window possible that is easy to access. It would be not so great to try and custom code a bunch of stuff for all of that.
Want results from more Discord servers?
Add your server