fvtt-performance-safe-updates
This skill should be used when adding features that update actors or items, implementing hook handlers, modifying update logic, or replacing embedded documents. Covers ownership guards, no-op checks, batched updates, queueUpdate wrapper, atomic document operations, and letting Foundry handle renders automatically for multi-client sync.
What this skill does
# Foundry VTT Performance-Safe Updates
Ensure document updates in Foundry VTT modules don't cause multi-client update storms or render cascades.
## When to Use This Skill
Invoke this skill when implementing ANY of the following in a Foundry VTT module:
- Adding a new feature that updates actors or items
- Modifying existing update logic
- Adding UI elements that trigger document changes
- Implementing hook handlers that respond to document changes
- Replacing or swapping embedded documents (abilities, items, effects)
## Core Problem
Foundry VTT runs in multi-client sessions where hooks fire on ALL connected clients. Without proper guards:
- Every client triggers duplicate updates (2-10x redundant database writes)
- Update storms occur when updates trigger more updates across clients
- UI flickers when delete+create patterns cause "empty state" renders between operations
- Performance degrades exponentially with number of connected clients
## The Performance-Safe Pattern
### Step 1: Ownership Guards
**Before any document update, ask: "Should this run on every client?"**
```javascript
// ❌ BAD: Runs on every connected client
Hooks.on("deleteItem", (item, options, userId) => {
item.parent.update({ "system.someField": newValue });
});
// ✅ GOOD: Only owner/GM performs the update
Hooks.on("deleteItem", (item, options, userId) => {
if (!item.parent?.isOwner) return;
item.parent.update({ "system.someField": newValue });
});
```
**Common ownership checks:**
- `item.isOwner` - Current user owns this item
- `item.parent?.isOwner` - Current user owns the parent (actor/container)
- `actor.isOwner` - Current user owns this actor
- `game.user.isGM` - Current user is the GM
**Use GM-only guards for:**
- World-level changes
- Compendium updates
- Global settings modifications
### Step 2: Skip No-Op Updates
**Before calling update, check if the value actually changes:**
```javascript
// ❌ BAD: Always updates, even if value unchanged
await actor.update({ "system.selected_load_level": newLevel });
// ✅ GOOD: Skip if already set
if (actor.system.selected_load_level === newLevel) return;
await actor.update({ "system.selected_load_level": newLevel });
```
**For flag-based updates:**
```javascript
// ✅ Skip if flag already matches target state
const currentProgress = actor.getFlag('bitd-alternate-sheets', 'abilityProgress') || {};
if (currentProgress[abilityId] === targetValue) return;
await actor.setFlag('bitd-alternate-sheets', 'abilityProgress', {
...currentProgress,
[abilityId]: targetValue
});
```
### Step 3: Batch Multiple Updates
**Combine multiple field changes into a single update call:**
```javascript
// ❌ BAD: Three separate updates (3x hooks, 3x database writes)
await actor.update({ "system.harm.level1.value": "Bruised" });
await actor.update({ "system.stress.value": 5 });
await actor.update({ "system.xp.value": 3 });
// ✅ GOOD: Single batched update
await actor.update({
"system.harm.level1.value": "Bruised",
"system.stress.value": 5,
"system.xp.value": 3
});
```
### Step 4: Use queueUpdate Wrapper
**Wrap ALL document updates in queueUpdate to prevent concurrent update collisions:**
```javascript
import { queueUpdate } from "./update-queue.js";
// ✅ Prevents race conditions in multi-client sessions
await queueUpdate(async () => {
await this.actor.update(updates);
});
```
**What queueUpdate does:**
- Ensures updates execute sequentially, not concurrently
- Prevents "lost update" race conditions
- Automatically handles update conflicts
**When to use:**
- ANY actor.update() call
- ANY updateEmbeddedDocuments() call
- Batch operations that modify multiple documents
### Step 5: Atomic Embedded Document Updates
**When replacing embedded documents (items, effects), NEVER use delete+create:**
```javascript
// ❌ BAD: Delete + Create causes UI flicker and race conditions
await actor.deleteEmbeddedDocuments("Item", [oldItemId]);
await actor.createEmbeddedDocuments("Item", [newItemData]);
// UI renders "empty state" between these calls!
// ✅ GOOD: Update in place (atomic operation)
await actor.updateEmbeddedDocuments("Item", [{
_id: oldItemId,
name: newItemData.name,
img: newItemData.img,
system: newItemData.system
}]);
```
**Use cases:**
- Swapping crew abilities
- Changing hunting grounds
- Replacing playbook items
- Updating item references
### Step 6: Guard Rerenders in Hooks
**Only rerender sheets that are owned and currently visible:**
```javascript
// ❌ BAD: Rerenders ALL character sheets (including closed/unowned)
Hooks.on("renderBladesClockSheet", (sheet, html, data) => {
game.actors.forEach(actor => {
actor.sheet.render(false);
});
});
// ✅ GOOD: Only rerender owned, open sheets
Hooks.on("renderBladesClockSheet", (sheet, html, data) => {
game.actors.forEach(actor => {
if (actor.isOwner && actor.sheet.rendered) {
actor.sheet.render(false);
}
});
});
```
### Step 7: Let Foundry Handle Renders (Avoid { render: false })
**Default behavior:** When `document.update()` is called, Foundry automatically re-renders all registered sheets on ALL connected clients. This is the correct behavior for multi-client synchronization.
**Understanding Foundry's render flow:**
When `document.update()` is called, Foundry:
1. Sends update to server
2. Broadcasts change to all clients
3. Fires `updateActor`/`updateItem` hooks on each client
4. Automatically calls `render()` on sheets registered in `doc.apps`
**Critical:** The `{ render: false }` option suppresses step 4 on **ALL clients**, not just the initiating client. This breaks multi-client synchronization.
```javascript
// ❌ BAD: Suppresses render on ALL clients, breaking multi-client sync
await actor.update({ "system.value": newValue }, { render: false });
// Other players' sheets won't update!
// ✅ GOOD: Let Foundry handle renders automatically
await queueUpdate(async () => {
await actor.update({ "system.value": newValue });
});
// All clients re-render automatically, staying in sync
```
**Only exception - Data Migrations in getData():**
When migrating data inside `getData()`, you must suppress render to prevent infinite loops:
```javascript
// In getData() - migration MUST suppress render to avoid infinite loop
async getData() {
// Detect old data format that needs migration
if (this.actor.system.oldField !== undefined) {
queueUpdate(() => this.actor.update({
"system.newField": this.actor.system.oldField,
"system.-=oldField": null
}, { render: false }));
}
// ... rest of getData
}
```
**With proper caching, Foundry sheet renders are fast (~2-3ms).** There's no need for "optimistic UI" patterns that manipulate DOM before/after updates.
### Step 8: Use the safeUpdate Helper
**Combine all guards into a single helper:**
```javascript
/**
* Safely updates a document with ownership and no-op guards.
* Lets Foundry handle re-renders automatically for multi-client sync.
*/
export async function safeUpdate(doc, updateData, options = {}) {
// 1. Ownership guard - only owner should update
if (!doc?.isOwner) return false;
// 2. Empty update guard
const entries = Object.entries(updateData || {});
if (entries.length === 0) return false;
// 3. No-op detection - skip if values unchanged
const hasChange = entries.some(([key, value]) => {
// Objects always treated as changes (too complex to deep-compare)
if (value !== null && typeof value === "object") return true;
const currentValue = foundry.utils.getProperty(doc, key);
return currentValue !== value;
});
if (!hasChange) return false;
// 4. Queued update - let Foundry handle renders
await queueUpdate(async () => {
await doc.update(updateData, options);
});
return true;
}
```
**Usage:**
```javascript
// Standard pattern: handles all guards, Foundry re-renders all clients
await safeUpdate(doc, { "system.value": newValue });
// Only use render: false for data migrations in getData()
await safeUpdate(doc, migratRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.