T
TyphonJS•2y ago
geoidesic

Hooks in svelte component? (not really a typhon question but nowhere else to ask :)

In my foundry index.js I'm listening for hook:
Hooks.on('renderChatMessage', doSomething);
Hooks.on('renderChatMessage', doSomething);
However I want to also listen for that hook in a chat card svelte component (one that is already rendered to the Chat panel):
<script>
function catchRolls(message, html) {
console.log("catchRolls");
}

function clickTargetButton() {
contest.toggle();
console.log("isContest", $contest);
Hooks.off("renderChatMessage", catchRolls);
Hooks.on("renderChatMessage", catchRolls);
}
</script>
<template>
TargetButton(targetCallback="{clickTargetButton}")
<script>
function catchRolls(message, html) {
console.log("catchRolls");
}

function clickTargetButton() {
contest.toggle();
console.log("isContest", $contest);
Hooks.off("renderChatMessage", catchRolls);
Hooks.on("renderChatMessage", catchRolls);
}
</script>
<template>
TargetButton(targetCallback="{clickTargetButton}")
While the hook in the index triggers, the one in the component does not. What am I missing?
18 Replies
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
I'll take a look later today. Usually I'm pretty prompt on replying to messages on this server, but I've been working in crunch mode / around the clock with unusual hours, so was zonked out most of yesterday after a 22 hour coding push ending around 10am PST. I did see that you tried to ask the question on the system devs main Foundry channel. You aren't going to get any valuable information or help for the most part regarding anything Svelte related on the mothership. In general though I'd strongly discourage you from registering with Foundry hooks inside a component. Almost always you should have any logic like catching roll information facilitated through a single management hook on the JS side of things and create a Svelte store that is injected as a prop into or through a context Map for any chat based Svelte components. If you absolutely have to register with a hook in a Svelte component you need to provide an onDestroy handler to unregister from the hook when the component is destroyed. I'll take a look though and test out a few things later today though as you should be able to register Hooks inside the script portion of a Svelte component. Maybe there is some funny business going on / maybe not.
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
I had zero problems getting hooks registered with the following example modified from the basic chatmessage example in essential-svelte-esm. Note the use of onDestroy. As mentioned you'd be better served in creating a single store that you inject into chat message components that is managed by a single hook callback in JS code where you update the store value as necessary based on roll data you want to manipulate / display . I strongly discourage you from registering within Svelte components unless you know exactly what you are doing and accompany that with a proper onDestroy callback. Here is the example I used which worked fine:
No description
geoidesic
geoidesic•2y ago
Ok that sounds really good. But the problem I have is that I can't find a place where I can both listen for the hook AND have access to the store? So e.g. most of my hook listeners are in index.js... but because that's not a svelte template, I can't manipulate the store from there (as I understand it). That's the reason I moved the hooks into the component the way I did but you are correct, I'm having trouble preventing multiple listeners from being registered as the onDestroy doesn't always seem to run (e.g. during HMR).
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
That is another reason why you should use a store. Review the Svelte docs. You can pass props and context Maps to a Svelte component when you create it. Presumably you are using the renderChatMessage hook to attach a Svelte component to the chat message HTML. https://svelte.dev/docs#run-time-client-side-component-api-creating-a-component myStore.js
import { writable } from 'svelte/store';

export const myStore = writable(false);
import { writable } from 'svelte/store';

export const myStore = writable(false);
import { myStore } from './myStore.js';

Hooks.on('renderChatMessage', (message, html) =>
{
const flagData = message.getFlag('your-package-name', 'data');

if (typeof flagData === 'object')
{
message._svelteComponent = new MyChatMessage({ target: html[0], props: { ...flagData, myStore });
}
});
import { myStore } from './myStore.js';

Hooks.on('renderChatMessage', (message, html) =>
{
const flagData = message.getFlag('your-package-name', 'data');

if (typeof flagData === 'object')
{
message._svelteComponent = new MyChatMessage({ target: html[0], props: { ...flagData, myStore });
}
});
Define a myStore export / prop in your chat component. Then anywhere on the JS side of things... myStore.set(true) and your component is updated via the store.
geoidesic
geoidesic•2y ago
Thanks! I'll give that a go. I was under the impression that I can't import stores outside of svelte components but perhaps I was wrong there.
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
You should definitely go back and work through the tutorials and read the Svelte docs; they aren't long. The svelte libraries are accessible from JS and you can work with stores with JS only. Svelte just adds syntactic sugar to using them in components.
geoidesic
geoidesic•2y ago
Thanks again. I do so periodically (go through the Svelte tutorial) but I'm not all that good at putting 2 and 2 together. I learn mostly by implementation, not by documentation. Anyhow I'll get there eventually!
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
Typically that is not how software development works exactly.. 😉
geoidesic
geoidesic•2y ago
Let me see that study where all the developers of the world have been questioned about their methods and we then can put a typically statistic to it 😉 But having said that, atypical is something I'll accept as a compliment! 🙂
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
Rather than just flippantly chastise your approach I'll delve into some real talk instead. I'll start with the things that are generally important to me, but mostly existential concerns. The biggest issue with TRL / Svelte within the larger Foundry community presently is social proof. There are only a handful of significantly trafficked packages using TRL. More or less Wasp's packages, Auto Animations, and to some extent Module Compatibility Checker. This is not enough to tip the scales of social proof yet through shear excellence in packages available that use TRL. I certainly do appreciate the early developer community that is forming around TRL, but things have been stagnating for a while now w/ few new developers arriving and social proof has a bit to do with that. Let's now talk a bit about what the Foundry 3rd party development "scene" is like. It very much is a "scene" as there is an utter lack of "industry" extended to 3rd party developers. When I talk of industry I mean the ability for 3rd party developers to fairly charge and receive an equitable exchange for the time invested into the platform & community; having the chance to break even on time invested is the bare minimum stepping stone to a more industrious community. I'm very doubtful at this point the Foundry platform will evolve in this direction unless there is a change from the top. In any community regardless of programming related or not when things are a "scene" the community is organized around tribal boundaries. This is the way of things from the bottom to the top of the larger Foundry community including the core team. As far as I can tell the core team views TRL and my efforts more as an existential threat rather than as a valuable contribution to the community. That sentiment flows down through the tribal nature of the community to the "talking heads" that frequent the mothership. When you post cluelessly on the Foundry forums about your lack of understanding the talking heads backhandedly at times will attack TRL / Svelte or at the very least are dismissive of the tech I'm developing though they lack any understanding about it and this has a ripple effect through the rest of the community. Your lack of understanding both of Foundry, but in combination w/ confusion about Foundry and TRL / Svelte more likely than not isn't interpreted for what it is, even intentionally misinterpreted, and possibly does more damage to the social proof that TRL / Svelte is viable than the current level / handful of higher trafficked TRL based packages brings to the table. I mean this is a deep topic and can go on for a bit. You must understand that I can't directly post or even mention TRL / Svelte on the Foundry Discord server due to flaws in how the community is implicitly organized and this spans from top to bottom. So, when you post on the Foundry Discord completely confused the surface level interpretation is that TRL / Svelte is confusing and not viable. My work is being treated as an outsider phenomenon. "Visionary status" is being afforded to the core team and there is no room existentially speaking otherwise. Want my opinion on that? I think it is sad. At the end of the day I'm still involved w/ Foundry at large not to seek some visionary status / ego boost, but because the engineering required to build a robust runtime library on top of Foundry and then generalize the architecture so that it can run on other environments transparently (SvelteKit and beyond) is a valuable litmus test to the flexibility of my work. It also allows me to harden TRL considerably before a wider public launch to the larger Svelte community making sure things are rock solid before such an effort commences. If it weren't for this I'd be long gone from the Foundry community as I don't see any future participating on the platform with the current status quo. In other words, the platform is capturing all the value from 3rd party devs and not making it equitable one bit. --- So if you can please do provide a bit of minimum consideration and be patient and ask TRL / Svelte related questions on this server and don't double post on the Foundry server even the League of Extraordinary Foundry Developers server is pointless to post on re: TRL / Svelte. There is no one at either server that can help you w/ TRL / Svelte that already isn't here and at this time I can respond to your posts here as the dev community is small enough. --- Now I'm not going to try and dig in too deep here on the chastising part, but I am going to point out that the attitude and approach you espoused of not reading documentation and throwing things at the wall versus seeking knowledge and a better fundamental understanding of how programming works falls squarely in the category of being a "script kiddie". There is no survey of developers that includes "script kiddie" behavior as being a valid or typical approach as those that are on this path are not considered software developers; professional or otherwise. I realize this can come across as harsh, but it's the truth from my perspective. As I mentioned before I do appreciate your enthusiasm. Presently, TRL favors those that do have a clue about software development and can connect the dots independently, but it is valuable to have all levels of skill represented in the TRL community as at the very least it gives me an idea of the types of tutorial resources I need to develop on the march toward "beta". These kinds of resources are still 6-12 months out. This upcoming release though is going to feature in-depth API docs for TRL and this is not something I expected to have until the final stages of reaching "beta", so that is a pleasant surprise. I do hope you read them to become more aware in general. I'd be glad to start including illustrative examples and topic discussion in the API docs for things you don't understand and there will be a lot! That is how you can provide a valuable contribution while trying to catch up in general.
geoidesic
geoidesic•2y ago
That's fair enough. I can post here instead of there if you prefer. But that being said I doubt that my postings have had any effect on your standing. If the community is tribally toxic then nothing will prevent that from permeating everything. The point of these channels is to support those who need help, and I certainly need help. So I won't stop posting in order to get help. i'm sure you can appreciate that. I have this hook now in index.js, which writes to the targets store:
Hooks.on("targetToken", (User, Token, targeted) => {
const unsubscribe = isContest.subscribe((obj) => {
Object.entries(obj).forEach(([id, state]) => {
const cardUsers = findKeysByValue(Token.actor.ownership, 3);
if (cardUsers.includes(User._id)) {
if (state === true) {
targets.add(id, getUserTargets(User).map(t => ({ avatar: t.document.texture.src, actor: t.actor })))
}
}
})
})
unsubscribe();
})
Hooks.on("targetToken", (User, Token, targeted) => {
const unsubscribe = isContest.subscribe((obj) => {
Object.entries(obj).forEach(([id, state]) => {
const cardUsers = findKeysByValue(Token.actor.ownership, 3);
if (cardUsers.includes(User._id)) {
if (state === true) {
targets.add(id, getUserTargets(User).map(t => ({ avatar: t.document.texture.src, actor: t.actor })))
}
}
})
})
unsubscribe();
})
It works well enough but the trouble is that I need to persist the targets to the chat log for the chat card that I'm creating. When I try to do that I get a bit of an exponential memory leak that I haven't quite figured out. In the ChatTemplate, I have this:
$: if ($isContest) {
console.log('update message');
message.update({
"flags.surge.data.targets": $targets[message._id] || [],
});
}
$: if ($isContest) {
console.log('update message');
message.update({
"flags.surge.data.targets": $targets[message._id] || [],
});
}
The problem with this is that each time it gets called (i.e. when targeting or untargeting a token). That console.log gets output double the number of times. So first target: 1, second target: 2, 3rd target: 4, etc. Although I think I may have a solution... move the subscription out of the hook and use it to generate a function which the hook then calls.
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
But that being said I doubt that my postings have had any effect on your standing.
It's not particularly about my standing. I'm just trying to control as much as possible the narrative about TRL / Svelte in relation to being a viable option for 3rd party Foundry dev. Negative information has a way of being elevated over positive information in general. For the time being while I have limited agency to talk about TRL on the main Foundry forums those that do are implicitly representing and affecting the "social proof". If you are the only one posting on those forums completely confused that has more weight. Do realize that developers are the target users of TRL and you are posting on the developer forums on the Foundry Discord. No one there can help you w/ TRL & Svelte. Those forums are appropriate for asking Foundry API specific questions. You will have to put a little work into formulating your questions there to just specifically ask about Foundry API details. You probably don't realize the implied negative associations of your posts there. The success stories IE positive information regarding those using TRL is significantly less visible in the Foundry community. Those using TRL certainly aren't required to announce that they are using TRL; it's always nice to give a nod, but none of that is visible in general on the main Foundry developer forums. So, in general the larger Foundry developer community doesn't know about success stories. --- Re: the code you just posted. You probably once again should take a look at the Svelte tutorials and learn more about how stores work.
The writable store implementation does work for simple use cases and is just a helper implementation of the store contract for basic use cases. For more advanced use cases it's often a good idea to write your own custom store implementation. Particularly if you need to retrieve the current state of the store and run encapsulated logic when the store state changes. The pattern above of temporarily subscribing to presumably get the store value mutating something then unsubscribing is not the way. That is an "anti-pattern". There is get method which is a helper to get a store value, but also is generally considered an anti-pattern. Behind the scene it will subscribe / unsubscribe returning the value of the store. That would just make your not efficient code more readable.
import { get } from 'svelte/store';

const value = get(isContest);
import { get } from 'svelte/store';

const value = get(isContest);
I can't exactly tell what your code block is intending to do. It looks like there is a bare data value targets which presumably is the data held also by a targets store and you are trying to combine things across two separate stores isContest and a targets store for state. Unfortunately I don't necessarily have the time to debug or write code for you. I think you should step back and thoroughly read up about Svelte stores. Anything more than basic use cases writing your own custom store is the way to go. https://svelte.dev/tutorial/custom-stores https://svelte.dev/docs#run-time-svelte-store This is a reasonable source for more info on custom stores: https://svelte-recipes.netlify.app/stores/ There are several custom stores from simpler to really complex in TRL. I'll try and find an example of a simpler one and post the link to that later today.
geoidesic
geoidesic•2y ago
Thanks. I have built custom stores etc. However the problem seems to be elsewhere. As soon as I use any kind of reactivity trigger within the ChatTemplate in order to update the Foundry message.flags, I start getting this memory leak. So I think the problem is more fundamental – i.e. it's not possible to update a message from within the message template component. If that makes sense? So for example... Here's the gameTargets store:
function createTargetStore() {
return writable(false);
}
export const gameTargets = createTargetStore()
function createTargetStore() {
return writable(false);
}
export const gameTargets = createTargetStore()
Here's the targetToken hook in index.js:
Hooks.on("targetToken", (User, Token, targeted) => {
gameTargets.set(game.user.targets);
});
Hooks.on("targetToken", (User, Token, targeted) => {
gameTargets.set(game.user.targets);
});
Very simple, just store all the game targets to a store to make it reactive for the ChatTemplate. Then I have the standard TRL ChatTemplate hook in index.js:
Hooks.on('renderChatMessage', (message, html) => {
// Find associated flag data scoped to your module or system ID. This is the easiest way to determine that this message is
// associated with your module and has a Svelte component attached to the message content.
const flagData = message.getFlag('surge', 'data');
if (typeof flagData === 'object') {
// Add the svelte component to the message instance loaded in client side memory.
message._svelteComponent = new SurgeRoll({ target: html[0], props: { ...flagData, message: message } });
}
});

Hooks.on('preDeleteChatMessage', (message) => {
// Find associated flag data scoped to your module ID. This is the easiest way to determine that this message is
// associated with your module and has a Svelte component attached to the message content.
const flagData = message.getFlag('surge', 'data');
// Also ensure that the Svelte component exists
if (typeof flagData === 'object' && typeof message?._svelteComponent?.$destroy === 'function') {
// Manually destroy Svelte component when the chat message document is being deleted.
message._svelteComponent.$destroy();
}
});
Hooks.on('renderChatMessage', (message, html) => {
// Find associated flag data scoped to your module or system ID. This is the easiest way to determine that this message is
// associated with your module and has a Svelte component attached to the message content.
const flagData = message.getFlag('surge', 'data');
if (typeof flagData === 'object') {
// Add the svelte component to the message instance loaded in client side memory.
message._svelteComponent = new SurgeRoll({ target: html[0], props: { ...flagData, message: message } });
}
});

Hooks.on('preDeleteChatMessage', (message) => {
// Find associated flag data scoped to your module ID. This is the easiest way to determine that this message is
// associated with your module and has a Svelte component attached to the message content.
const flagData = message.getFlag('surge', 'data');
// Also ensure that the Svelte component exists
if (typeof flagData === 'object' && typeof message?._svelteComponent?.$destroy === 'function') {
// Manually destroy Svelte component when the chat message document is being deleted.
message._svelteComponent.$destroy();
}
});
Then in the ChatTemplate.svelte I have the following reactive statement (where $gameTargets is the reactive reference to the gameTargets store):
import { gameTargets } from "~/stores/contests";
$: if ($gameTargets) {
message.update({
flags: {
surge: {
data: {
targets: Array.from($gameTargets).map((target) => {
return {
avatar: target.document.texture.src,
actor: { name: target.document.name, uuid: target.document.uuid },
};
}),
},
},
},
});
console.log("message", message.flags.surge.data.targets);
}
import { gameTargets } from "~/stores/contests";
$: if ($gameTargets) {
message.update({
flags: {
surge: {
data: {
targets: Array.from($gameTargets).map((target) => {
return {
avatar: target.document.texture.src,
actor: { name: target.document.name, uuid: target.document.uuid },
};
}),
},
},
},
});
console.log("message", message.flags.surge.data.targets);
}
So all it is doing is updating the message and storing some flags from the targets reactively. However, this causes the memory leak. Maybe it's because I'm passing message into message here?
message._svelteComponent = new SurgeRoll({ target: html[0], props: { ...flagData, message: message } });
message._svelteComponent = new SurgeRoll({ target: html[0], props: { ...flagData, message: message } });
But I'm not sure how else to make that reference available within the ChatTemplate. Maybe I need to find a way to write the flags directly in the renderChatMessage hook, rather than within the template? But I'm not sure how to do that reactively in response to new targets.
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
It is entirely unclear why you'd need to store and constantly update flags on the message document. This code would seemingly trigger repeatedly when targeting changes for all SurgeRoll components loaded into the chat log in perpetuity. It's one thing to have game targets in a store and you should do the preprocessing up front:
Hooks.on("targetToken", (User, Token, targeted) => {
gameTargets.set(Array.from(game.user.targets).map((target) => {
return {
avatar: target.document.texture.src,
actor: { name: target.document.name, uuid: target.document.uuid },
};
}));
});
Hooks.on("targetToken", (User, Token, targeted) => {
gameTargets.set(Array.from(game.user.targets).map((target) => {
return {
avatar: target.document.texture.src,
actor: { name: target.document.name, uuid: target.document.uuid },
};
}));
});
I don't really have a clue what you are going for in the code you provided. What is the end goal of having the targets represented in the chat message? Ostensibly, let's say you have a roll chat message appear in the chat log. Wouldn't you have an apply button to apply whatever it is you are attempting to do w/ the target data? It's only after a conceivable apply button press that you'd want to store the selected targets as flags to the chat message. If there are set flags on the chat message then you display the baked in targeting data from flags. Otherwise you show a dynamic display of current targeting data until the apply button is pressed. I'd send the UUID of the chat message to the Svelte component and only retrieve it temporarily when the apply button is pressed.
geoidesic
geoidesic•2y ago
Ok, only store flags on apply. I'll give that a go, tx.
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
You also need to consider what things will look like for other players who aren't the active user manipulating the SurgeRoll component. You'll have to be able to distinguish which player is the owner and perhaps show a "targeting" message instead of the changing targets for all other users. When the flag data is updated then you display the targets to all other players for that roll. In that case you might have to retrieve the message document and use TJSDocument to get updates to read the flag change.
geoidesic
geoidesic•2y ago
The above has been very helpful, thought-provoking and useful. While I accept that it might be better to avoid this issue for the sake of practicality, I'd also really like to understand why it happens. I.e. If I use a reactive statement within the Chat Message component that listens to incoming target events to then write a map of those targets to the Chat Message game object, then each time I click a target, the amount of times the reactive statement runs is doubled. Why would that be? I've moved the mapping into the hook as you suggested:
Hooks.on("targetToken", async (User, Token, targeted) => {
const targets = await Promise.all(
game.user.targets
.filter((target) => {
if (Token._id === target._id && target == false) return false;
return true;
})
.map(async (target) => {
const mapped = {
avatar: target.document.texture.src,
actor: target.actor,
tokenUuid: target.document.uuid,
actorId: target.actorId,
items: getContestItems(target.actor.items),
hitLocation: await getHitLocation(),
clickedByUserId: User._id,
};
return mapped;
})
);

gameTargets.set(targets);
});
Hooks.on("targetToken", async (User, Token, targeted) => {
const targets = await Promise.all(
game.user.targets
.filter((target) => {
if (Token._id === target._id && target == false) return false;
return true;
})
.map(async (target) => {
const mapped = {
avatar: target.document.texture.src,
actor: target.actor,
tokenUuid: target.document.uuid,
actorId: target.actorId,
items: getContestItems(target.actor.items),
hitLocation: await getHitLocation(),
clickedByUserId: User._id,
};
return mapped;
})
);

gameTargets.set(targets);
});
Which is elegant as it simplifies the component. Then in the component I have this:
function getUserTargets(User, targets) {
const targetsArray = Array.from(targets);
if (targetsArray.length == 0) return []

return targetsArray.filter((target) => {
return target.clickedByUserId === User._id
});
}

async function updateTargets(targets) {
console.log("targets", targets);
await message.update({
flags: {
surge: {
data: {
targets: targets,
},
},
},
});
}

$: currentValidCanvasTargets = getUserTargets(message.user, $gameTargets);
$: updateTargets(currentValidCanvasTargets);
function getUserTargets(User, targets) {
const targetsArray = Array.from(targets);
if (targetsArray.length == 0) return []

return targetsArray.filter((target) => {
return target.clickedByUserId === User._id
});
}

async function updateTargets(targets) {
console.log("targets", targets);
await message.update({
flags: {
surge: {
data: {
targets: targets,
},
},
},
});
}

$: currentValidCanvasTargets = getUserTargets(message.user, $gameTargets);
$: updateTargets(currentValidCanvasTargets);
Now, as you suggested I could just run the update when a button is clicked. And that's cool but I'm quite curious to understand why this leak occurs. If it's not possible to know then fair enough but I thought perhaps the simpler code here might help to make the problem more evident. Where you say, you might have to retrieve the message document and use TJSDocument to get updates, where should that be done? Is it possible to do it in index.js?
Hooks.on('renderChatMessage', (message, html) => {
const flagData = message.getFlag('surge', 'data');
if (typeof flagData === 'object') {
message._svelteComponent = new SurgeRoll(
{
target: html[0],
props: {
...flagData,
message //<-- here maybe?
}
}
)
}
});
Hooks.on('renderChatMessage', (message, html) => {
const flagData = message.getFlag('surge', 'data');
if (typeof flagData === 'object') {
message._svelteComponent = new SurgeRoll(
{
target: html[0],
props: {
...flagData,
message //<-- here maybe?
}
}
)
}
});
Or would it need to be further downstream, in the ChatMessage template component?
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
While I accept that it might be better to avoid this issue for the sake of practicality,
Unfortunately I do not have the spare time to debug "impractical" things. It is best that you learn how to debug your own code in general. Look up the concept of "divide and conquer" in relation to debugging. https://markheath.net/post/effective-debugging-with-divide-and-conquer https://betterprogramming.pub/find-and-fix-bugs-like-a-pro-with-divide-and-conquer-d55f3cf91154 The likely reason though is because you don't understand that Foundry hooks are strictly synchronous. By doing multiple async things; first the hook callback being async then Promise.all with X promises resolving is causing timing issues between successive Hook targetToken callbacks. When you switch between targets if it is one to another multiple calls in quick succession happen to targetToken hook. What this ends up looking like is that store gets updated twice; once with empty data then new data when the Foundry hooks are called in quick succession due to your misunderstanding on how Foundry works. ----- Other notes... Keep in mind that Svelte components in an ideally constructed app are dumb. Updating flags in a document is not dumb. What you are going for can likely be better designed and accomplished through better or a variety of different techniques including sockets. I really can't help you or debug your code further. You need to learn how to do that yourself.
Want results from more Discord servers?
Add your server