Store
Un objeto, acceso por rutas con puntos, suscripciones acotadas por ruta. Apps escritas en React, Vue y Svelte leen y escriben el mismo estado sin importar nada la una de la otra.
import { wu } from 'wu-framework';// o los exports con nombre:import { getState, setState, onStateChange } from 'wu-framework';El store es un singleton de módulo: wu.store, el export por defecto del módulo
del store y los atajos getState/setState apuntan todos a la misma instancia.
get(path?: string): anyLee el valor en una ruta con puntos.
wu.store.get('user.profile.name'); // 'Ada'wu.store.get('user'); // { profile: { name: 'Ada' }, id: 7 }wu.store.get(); // el objeto de estado completoCasos límite. Devuelve por referencia, no una copia: mutar lo que
recibes muta el store, silenciosamente y sin notificar a nadie. Llamarlo sin
ruta (o con '', null, 0) devuelve el objeto de estado completo, igual de
mutable. Un intermedio inexistente da undefined en vez de lanzar. Cualquier
segmento de ruta igual a __proto__, constructor o prototype devuelve
undefined (protección contra contaminación de prototipos). Pasar un valor
truthy que no sea cadena lanza un TypeError.
set(path: string, value: any): numberEscribe un valor y devuelve su número de secuencia: un contador monótono que
empieza en 0 para la primera escritura.
const seq = wu.store.set('cart.items', [{ id: 1, qty: 2 }]);wu.store.set('user.profile.name', 'Ada'); // crea user y user.profileCasos límite. Varios, y son importantes:
- Los valores se guardan por referencia. Sin clonar, sin congelar. Si mutas después el objeto que pasaste, el store cambia y nadie se entera. Pasa un objeto nuevo por escritura si quieres que el seguimiento de cambios sea honesto.
- Sin comprobación de igualdad.
set('a', 1)dos veces realiza dos escrituras completas, dos números de secuencia y dos rondas de notificación. Nunca es un no-op. - Los intermedios que faltan se crean, y los intermedios que no son objetos
se sobrescriben: un
set('a', 1)seguido deset('a.b', 2)destruye el1. - Lanza
[WuStore] Unsafe key in path: "<segment>" (path: "<path>")para__proto__,constructoroprototypeen cualquier punto de la ruta. - Una ruta falsy reemplaza todo el estado.
set('', v)funciona. Peroset(null, v)yset(undefined, v)también reemplazan el estado y luego lanzan unTypeErrorno capturado desde dentro del microtask de notificación. No hagas eso.
Momento de las notificaciones
Sección titulada «Momento de las notificaciones»Las escrituras se aplican de forma síncrona: un get() justo después de un
set() ve el valor nuevo. Los listeners se difieren a un microtask, uno por
cada llamada a set(), sin agrupar.
wu.store.set('count', 1);wu.store.get('count'); // 1 — inmediatamente// los listeners de 'count' se disparan al final de este tickAgrupar varias escrituras no agrupa sus notificaciones.
on(pattern: string, callback: Function): () => voidSe suscribe a una ruta o a un patrón con comodines. Devuelve una función para cancelar la suscripción.
La firma del callback depende del patrón
Sección titulada «La firma del callback depende del patrón»Esta es la parte más propensa a errores de toda la API del store, así que vale la pena decirlo sin rodeos.
| Patrón | El callback recibe |
|---|---|
Ruta exacta (sin *) |
Dos argumentos: (value, path) |
Contiene * en cualquier sitio |
Un objeto: ({ path, value }) |
// Ruta exacta — dos argumentos posicionaleswu.store.on('user.profile.name', (value, path) => { console.log(path, '=', value); // 'user.profile.name = Ada'});
// Comodín — un único objetowu.store.on('user.*', ({ path, value }) => { console.log(path, '=', value);});Equivocarse aquí falla en silencio: un listener con comodín escrito como
(value, path) recibe el objeto {path, value} como value y undefined como
path.
Burbujeo
Sección titulada «Burbujeo»Una escritura notifica a su ruta exacta y a todas las rutas ancestras, empezando por la más profunda.
wu.store.on('user', (value, path) => console.log('listener de user', value));wu.store.set('user.profile.name', 'Ada');// se disparan: los listeners de 'user.profile.name', luego 'user.profile', luego 'user'Los listeners ancestros reciben el objeto ancestro completo, leído de nuevo
en el momento de la notificación, no el valor de la hoja que se escribió. El
listener de user de arriba recibe { profile: { name: 'Ada' }, ... }, no
'Ada'.
El burbujeo solo aplica a los listeners de ruta exacta. Los comodines no
burbujean: on('user.*') no se dispara ante una escritura en
user.profile.name, porque el patrón se compara solo contra la ruta escrita y
* no puede cruzar un punto. Burbujeo y comodines son mecanismos separados y
sin solape.
Sintaxis de comodines
Sección titulada «Sintaxis de comodines»| Patrón | Coincide con | No coincide con |
|---|---|---|
* |
Todo (caso especial) | — |
user.* |
user.name, user.email |
user, user.profile.name |
*.name |
user.name, org.name |
a.b.name |
a.*.c |
a.x.c |
a.c, a.x.y.c |
Un segmento * significa exactamente un segmento no vacío y sin puntos. El
número de segmentos es fijo y la coincidencia está anclada por ambos extremos.
No existe **. user.** se escapa a un literal y solo coincide con una
ruta literalmente llamada user.**. Los globs parciales tampoco funcionan:
user.na* es un literal, no una coincidencia por prefijo. Fíjate en que esta
gramática es distinta de la del bus de eventos, donde * se expande a .*
sobre cualquier carácter.
Casos límite. on() nunca reproduce el estado actual: quien se suscribe no
recibe nada hasta la siguiente escritura. Registrar dos veces la misma
referencia de función colapsa (el almacenamiento es un Set), y una sola baja la
elimina. Un listener que lanza se captura y se registra; nunca detiene a los
demás. Darse de baja es idempotente.
batch()
Sección titulada «batch()»batch(updates: Record<string, any>): number[]Aplica varias escrituras y devuelve sus números de secuencia en orden de inserción.
wu.store.batch({ 'user.name': 'Ada', 'user.email': 'ada@example.com', 'ui.theme': 'dark',});Casos límite. Pese al nombre, esto no es atómico ni agrupado: es un bucle
sobre set(). N escrituras, N microtasks, N rondas de notificación. Si una ruta
contiene una clave insegura, el bucle lanza a mitad de camino, dejando aplicadas
las escrituras anteriores y perdiendo el array devuelto. Trátalo como una
comodidad, no como una transacción.
clear()
Sección titulada «clear()»clear(): voidReinicia el estado a {}, descarta todos los listeners y reinicia el cursor de
escritura.
wu.store.clear();Casos límite. No reinicia las métricas, no elimina los taps y no
detiene un sync() activo: las escrituras posteriores al clear se seguirán
difundiendo a los peers. wu.destroy() lo llama como parte del desmontaje.
getMetrics()
Sección titulada «getMetrics()»getMetrics(): { reads: number; writes: number; notifications: number; bufferUtilization: number; sequenceNumber: number; bufferSize: number; listenerCount: number;}console.log(wu.store.getMetrics());Casos límite. listenerCount cuenta rutas y patrones distintos, no
callbacks: diez listeners en una misma ruta cuentan como uno. notifications
cuenta rondas de despacho, no callbacks invocados. Las métricas sobreviven a
clear().
getRecentEvents()
Sección titulada «getRecentEvents()»getRecentEvents(count?: number): Array<{ path: string, value: any, timestamp: number }>Las últimas escrituras, las más recientes primero, desde un búfer circular de 256 posiciones.
console.table(wu.store.getRecentEvents(20));Casos límite. El búfer es un anillo de tamaño fijo: cualquier cosa más
antigua que 256 escrituras ya se sobrescribió. value es la referencia viva
guardada en la posición, así que las entradas pueden estar desactualizadas.
timestamp viene de performance.now() — relativo a la carga de la página, no
un epoch.
sync(opts?: { transport?: 'broadcast' | WebSocket | string | { send, onMessage, close }; room?: string;}): SyncHandleEstado colaborativo en tiempo real. Cada escritura posterior a la llamada se sella con un reloj de Lamport y un id de sitio, se difunde a los peers y se fusiona con la política “gana el último que escribe” por ruta.
// Sincronizar entre pestañas del mismo origenconst handle = wu.store.sync();
// O entre clientesconst handle = wu.store.sync({ transport: 'wss://sync.example.com', room: 'doc-42' });
await handle.ready();handle.status(); // { connected, site, lamport, peers, sent, received, ... }handle.stop();El handle se devuelve de forma síncrona mientras el chunk del CRDT se carga
en segundo plano. ready() resuelve a la instancia, o a null si la conexión
falló — nunca se rechaza.
Casos límite y limitaciones reales:
- El estado escrito antes de
sync()nunca se propaga. Solo se rastrean y se envían a los que llegan tarde las rutas escritas después de la llamada. - Los reemplazos de estado completo nunca se sincronizan: un
set('', value)se queda en local. - Un segundo
sync()mientras hay uno activo devuelve el handle existente e ignora las opciones nuevas. Deténlo primero para reconfigurarlo. - Las rutas que se solapan no convergen de forma fiable. Escrituras
concurrentes a
usery auser.namedesde dos peers pueden producir resultados distintos en cada réplica. Escribe en las hojas. - Sobre WebSocket el formato de cable es JSON, así que
Datese convierte en una cadena ISO,Map/Setse convierten en{}, yNaN/Infinityse convierten ennull. BroadcastChannel usa clonado estructurado y los preserva. - Los valores
bigint,functionysymbolde primer nivel se descartan con un aviso. - Un fallo de conexión no lanza: se registra, y el handle simplemente nunca conecta.
tap(fn: (sequence: number, path: string, value: any) => void): () => voidUna manguera síncrona sobre todas las escrituras, disparada dentro de set()
antes del microtask de notificación. Esto es lo que usa el grabador de la
timeline.
const untap = wu.store.tap((seq, path, value) => { console.log(seq, path, value);});Casos límite. Un argumento que no sea función se ignora silenciosamente y se
devuelve una baja que es un no-op. Los taps no los dispara hydrate(),
deliberadamente: eso crearía un bucle de realimentación en el registro durante el
viaje en el tiempo. Los errores se capturan y se registran por cada tap.
Métodos que no existen
Sección titulada «Métodos que no existen»Vale la pena decirlo explícitamente, porque la gente los busca:
- No hay
delete(). Pon la ruta anulloundefineden su lugar. La clave sigue presente en el objeto. - No hay
has(). Usaget(path) !== undefined, asumiendo que no puede distinguir “ausente” de “asignado a undefined”. - No hay
off(): conserva la función que devolvióon(). - No hay
getAll(),reset(),subscribe(),snapshot(),select()nicomputed().
Limitaciones honestas
Sección titulada «Limitaciones honestas»- Semántica de referencias por todas partes.
get()entrega referencias vivas,set()las guarda. Nada se clona ni se congela. La disciplina corre de tu cuenta. - Existen dos gramáticas de comodines incompatibles en el framework: esta (por segmentos) y la del bus de eventos (por caracteres). Se parecen y se comportan distinto.
- La forma del callback cambia con el patrón. No hay ningún aviso en tiempo de ejecución cuando te equivocas.
- Sin transacciones.
batch()es un bucle; un fallo a mitad deja el estado parcial. - El store es global y no está particionado. Cualquier app puede escribir en cualquier ruta. No hay imposición de namespaces ni control de acceso — convenciones como prefijar con el nombre de tu app son responsabilidad tuya.
