Persistencia de progreso/settings del juego, compatible con iframes sandboxeados.
Por qué existe: la plataforma sirve los juegos uploaded en un
<iframe sandbox="allow-scripts ...">SINallow-same-origin. Eso es defense in depth — un juego de un dev externo no puede leer cookies/localStorage del parent ni atacar a otros jugadores. Pero también significa quelocalStoragedirecto del juego falla conSecurityError. Este módulo bridgea viapostMessageal parent, que guarda los datos namespaced por slug. Mismo API funciona en dev local (sin sandbox) — automágico.
Quickstart
import { storage } from "/sdk/v0.0.1-beta/jugafy.js";
// Esperar al hydrate inicial (datos llegan del parent en sandbox).
await storage.ready();
// Set / get / remove / clear.
storage.set("level", 5);
storage.set("settings", { sfx: true, music: 0.6 });
const lvl = storage.get("level"); // 5
const cfg = storage.get("settings"); // { sfx: true, music: 0.6 }
const k = storage.keys(); // ["level", "settings"]
storage.remove("level");
storage.clear(); // borra todo el namespace del juego
API
storage.ready(): Promise<void>
Resuelve cuando el cache está hidratado desde el parent. Llamala antes de cualquier get — antes del hydrate, get puede devolver undefined aunque haya valor guardado de sesiones previas.
En contexto same-origin (dev local con iframe.src apuntando a un path same-origin, o juego corriendo standalone fuera de iframe), ready() resuelve inmediato. En sandbox, espera hasta 1.5s al hydrate del parent — si no llegó (parent caído o desconectado), resuelve con cache vacío.
await storage.ready();
const score = storage.get("highscore") ?? 0;
storage.set(key, value): void
Setea un valor. Sync. value debe ser serializable con JSON.stringify (objetos, arrays, primitivos — no funciones, no Date, no Map).
key debe matchear /^[a-zA-Z0-9_.\-:]{1,128}$/. El parent rechaza keys que no cumplan.
storage.get(key): any | undefined
Lee el cache local. Sync. Devuelve undefined si no existe.
storage.remove(key): void
Elimina la key (cache + parent).
storage.clear(): void
Borra todas las keys del namespace (slug actual). No afecta otros juegos.
storage.keys(): string[]
Array de keys conocidas. En same-origin refleja el localStorage real. En sandbox refleja el cache local — await ready() antes para que esté completo.
storage.mode: "same-origin" | "sandboxed-bridge"
Diagnóstico. Útil para debuguear divergencias entre dev local (typically same-origin) y prod (sandboxed-bridge).
console.log("[mygame] storage mode:", storage.mode);
storage.slug: string
El slug del juego actual (lee window.NEON_GAME). Solo informativo — no necesitás pasárselo a las APIs.
Migration: localStorage → Jugafy.storage
Patrón típico ANTES (roto en sandbox)
// Boot
const saved = JSON.parse(localStorage.getItem("mygame_state") || "null");
if (saved) restoreState(saved);
// Game over
localStorage.setItem("mygame_state", JSON.stringify(currentState));
localStorage.setItem("mygame_highscore", score);
DESPUÉS (sandbox-safe)
import { storage } from "/sdk/v0.0.1-beta/jugafy.js";
// Boot
await storage.ready();
const saved = storage.get("state");
if (saved) restoreState(saved);
// Game over
storage.set("state", currentState);
storage.set("highscore", score);
Diferencias de comportamiento:
- No JSON.parse / stringify manual. El módulo lo hace internamente.
- Una llamada async al boot.
await storage.ready(). Después todo es sync. - Sin colisiones de namespace. Los datos de tu juego viven en
jugafy:store:<slug>:<key>del localStorage del parent. No te pisás con otros juegos ni con la plataforma.
Persistencia y límites
- Source-of-truth en sandbox: localStorage del dominio jugafy.com (parent). Si el user limpia el storage del dominio, pierde el progreso de TODOS los juegos del catálogo. Trade-off conocido.
- Same-origin (dev local): localStorage del propio iframe con prefix por slug. Persistente entre reloads, scoped por dominio.
- Cross-device: hoy NO hay sync server-side. Si el user juega en mobile y desktop con la misma cuenta, los progresos no se mergean automáticamente. Apuesta separada en el roadmap del SDK (sync a tabla
game_progressen Supabase para users logueados). - Quota: el browser impone ~5MB por dominio para localStorage. Compartido entre todos los juegos. No persistas binarios grandes — para eso usá Supabase Storage via edge function dedicada.
Caveats
-
El primer
getantes deready()devuelveundefined. Esto cambia comportamiento si tu boot leía localStorage síncrono. Si no podés awaitar (porque tu loop ya arrancó), suscribite a un evento de “datos cargados”:storage.ready().then(() => { // Acá ya es seguro hacer get() de cualquier key. const saved = storage.get("state"); if (saved) applySaved(saved); }); -
setes fire-and-forget. No esperás confirmación del parent. Si el parent está caído, los datos solo viven en el cache local de la tab actual hasta el próximo reload. Trade-off para no bloquear el game loop. -
Múltiples tabs del mismo juego: cada tab tiene su cache independiente. Cambios en una tab NO se propagan a la otra hasta un reload. Si necesitás multi-tab live sync, abrí issue en el repo del SDK.
-
JSON serializable únicamente: si guardás un objeto con métodos /
Date/Map, los recuperás como objeto plano sin esos campos.
Modo same-origin (dev local)
Cuando corras tu juego standalone (ej. python -m http.server en la carpeta del juego), el shim detecta que está en top-level (window.parent === window) y usa localStorage directo con prefix jugafy:store:<slug>:. Esto te permite desarrollar y testear sin la plataforma. Cuando subas el bundle a Jugafy, automáticamente cambia a modo bridge — sin cambios en tu código.
Para forzar window.NEON_GAME en dev local:
<script>window.NEON_GAME = "mi-juego";</script>
<script type="module" src="game.js"></script>
Versionado
Este módulo vive en v0.0.1-beta del SDK. La API es estable hasta que cambie a v1.0.0. Cambios breaking (si los hay) van a un nuevo path /sdk/v0.x.x-beta/ — los uploads ya servidos se quedan apuntando a su versión.