swl-ses 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +425 -0
- package/_userland/agentes/.gitkeep +0 -0
- package/_userland/habilidades/.gitkeep +0 -0
- package/agentes/accesibilidad-wcag-swl.md +683 -0
- package/agentes/arquitecto-swl.md +210 -0
- package/agentes/auto-evolucion-swl.md +408 -0
- package/agentes/backend-api-swl.md +442 -0
- package/agentes/backend-node-swl.md +439 -0
- package/agentes/backend-python-swl.md +469 -0
- package/agentes/backend-workers-swl.md +444 -0
- package/agentes/cloud-infra-swl.md +466 -0
- package/agentes/consolidador-swl.md +487 -0
- package/agentes/datos-swl.md +568 -0
- package/agentes/depurador-swl.md +301 -0
- package/agentes/devops-ci-swl.md +352 -0
- package/agentes/disenador-ui-swl.md +546 -0
- package/agentes/documentador-swl.md +323 -0
- package/agentes/frontend-angular-swl.md +603 -0
- package/agentes/frontend-css-swl.md +700 -0
- package/agentes/frontend-react-swl.md +672 -0
- package/agentes/frontend-swl.md +483 -0
- package/agentes/frontend-tailwind-swl.md +808 -0
- package/agentes/implementador-swl.md +235 -0
- package/agentes/investigador-swl.md +274 -0
- package/agentes/investigador-ux-swl.md +482 -0
- package/agentes/migrador-swl.md +389 -0
- package/agentes/mobile-android-swl.md +473 -0
- package/agentes/mobile-cross-swl.md +501 -0
- package/agentes/mobile-ios-swl.md +464 -0
- package/agentes/notificador-swl.md +886 -0
- package/agentes/observabilidad-swl.md +408 -0
- package/agentes/orquestador-swl.md +490 -0
- package/agentes/planificador-swl.md +222 -0
- package/agentes/producto-prd-swl.md +565 -0
- package/agentes/release-manager-swl.md +545 -0
- package/agentes/rendimiento-swl.md +691 -0
- package/agentes/revisor-codigo-swl.md +254 -0
- package/agentes/revisor-seguridad-swl.md +316 -0
- package/agentes/tdd-qa-swl.md +323 -0
- package/agentes/ux-disenador-swl.md +498 -0
- package/bin/swl-ses.js +119 -0
- package/comandos/swl/actualizar.md +117 -0
- package/comandos/swl/aprender.md +348 -0
- package/comandos/swl/auditar-deps.md +390 -0
- package/comandos/swl/autoresearch.md +346 -0
- package/comandos/swl/checkpoint.md +296 -0
- package/comandos/swl/compactar.md +283 -0
- package/comandos/swl/crear-skill.md +609 -0
- package/comandos/swl/discutir-fase.md +230 -0
- package/comandos/swl/ejecutar-fase.md +302 -0
- package/comandos/swl/evolucionar.md +377 -0
- package/comandos/swl/instalar.md +220 -0
- package/comandos/swl/mapear-codebase.md +205 -0
- package/comandos/swl/nuevo-proyecto.md +154 -0
- package/comandos/swl/planear-fase.md +221 -0
- package/comandos/swl/release.md +405 -0
- package/comandos/swl/salud.md +382 -0
- package/comandos/swl/verificar.md +292 -0
- package/habilidades/accesibilidad-a11y/SKILL.md +584 -0
- package/habilidades/angular-avanzado/SKILL.md +491 -0
- package/habilidades/angular-moderno/SKILL.md +326 -0
- package/habilidades/api-rest-diseno/SKILL.md +302 -0
- package/habilidades/api-rest-diseno/recursos/openapi-template.yaml +506 -0
- package/habilidades/aprendizaje-continuo/SKILL.md +369 -0
- package/habilidades/async-python/SKILL.md +474 -0
- package/habilidades/auth-patrones/SKILL.md +488 -0
- package/habilidades/auto-evolucion-protocolo/SKILL.md +376 -0
- package/habilidades/autoresearch/SKILL.md +248 -0
- package/habilidades/autoresearch/recursos/checklist-template.md +191 -0
- package/habilidades/autoresearch/scripts/calcular-score.js +88 -0
- package/habilidades/checklist-calidad/SKILL.md +247 -0
- package/habilidades/checklist-calidad/recursos/quality-report-template.md +148 -0
- package/habilidades/checklist-seguridad/SKILL.md +224 -0
- package/habilidades/checkpoints-verificacion/SKILL.md +309 -0
- package/habilidades/checkpoints-verificacion/recursos/checkpoint-templates.md +360 -0
- package/habilidades/ci-cd-pipelines/SKILL.md +583 -0
- package/habilidades/ci-cd-pipelines/recursos/github-actions-template.yaml +403 -0
- package/habilidades/cloud-aws/SKILL.md +497 -0
- package/habilidades/compactacion-contexto/SKILL.md +201 -0
- package/habilidades/contenedores-docker/SKILL.md +453 -0
- package/habilidades/contenedores-docker/recursos/dockerfile-template.dockerfile +160 -0
- package/habilidades/css-moderno/SKILL.md +463 -0
- package/habilidades/datos-etl/SKILL.md +486 -0
- package/habilidades/dependencias-auditoria/SKILL.md +293 -0
- package/habilidades/deprecacion-migracion/SKILL.md +485 -0
- package/habilidades/design-tokens/SKILL.md +519 -0
- package/habilidades/discutir-fase/SKILL.md +167 -0
- package/habilidades/diseno-responsivo/SKILL.md +326 -0
- package/habilidades/django-experto/SKILL.md +395 -0
- package/habilidades/doc-sync/SKILL.md +259 -0
- package/habilidades/ejecutar-fase/SKILL.md +199 -0
- package/habilidades/estructura-proyecto-claude/SKILL.md +459 -0
- package/habilidades/estructura-proyecto-claude/recursos/claude-md-template.md +261 -0
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +213 -0
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +77 -0
- package/habilidades/estructura-proyecto-claude/recursos/variantes-por-stack.md +177 -0
- package/habilidades/event-driven/SKILL.md +580 -0
- package/habilidades/extractor-de-aprendizajes/SKILL.md +234 -0
- package/habilidades/fastapi-experto/SKILL.md +368 -0
- package/habilidades/frontend-avanzado/SKILL.md +555 -0
- package/habilidades/git-worktrees-paralelo/SKILL.md +246 -0
- package/habilidades/iam-secretos/SKILL.md +511 -0
- package/habilidades/instalar-sistema/SKILL.md +140 -0
- package/habilidades/kubernetes-orquestacion/SKILL.md +549 -0
- package/habilidades/manejo-errores/SKILL.md +512 -0
- package/habilidades/mapear-codebase/SKILL.md +199 -0
- package/habilidades/microservicios/SKILL.md +473 -0
- package/habilidades/mobile-flutter/SKILL.md +566 -0
- package/habilidades/mobile-react-native/SKILL.md +493 -0
- package/habilidades/monitoring-alertas/SKILL.md +447 -0
- package/habilidades/node-experto/SKILL.md +521 -0
- package/habilidades/notificaciones-multicanal/SKILL.md +448 -0
- package/habilidades/notificaciones-multicanal/recursos/config-template.json +115 -0
- package/habilidades/nuevo-proyecto/SKILL.md +183 -0
- package/habilidades/patrones-python/SKILL.md +381 -0
- package/habilidades/performance-baseline/SKILL.md +243 -0
- package/habilidades/planear-fase/SKILL.md +184 -0
- package/habilidades/postgresql-experto/SKILL.md +379 -0
- package/habilidades/react-experto/SKILL.md +434 -0
- package/habilidades/react-optimizacion/SKILL.md +328 -0
- package/habilidades/release-semver/SKILL.md +226 -0
- package/habilidades/release-semver/scripts/generar-changelog.sh +238 -0
- package/habilidades/sql-optimizacion/SKILL.md +314 -0
- package/habilidades/tailwind-experto/SKILL.md +412 -0
- package/habilidades/tdd-workflow/SKILL.md +267 -0
- package/habilidades/testing-python/SKILL.md +350 -0
- package/habilidades/threat-model-lite/SKILL.md +218 -0
- package/habilidades/typescript-avanzado/SKILL.md +454 -0
- package/habilidades/ux-diseno/SKILL.md +488 -0
- package/habilidades/validacion-ci-sistema/SKILL.md +543 -0
- package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +286 -0
- package/habilidades/verificar-trabajo/SKILL.md +208 -0
- package/habilidades/wireframes-flujos/SKILL.md +396 -0
- package/habilidades/workflow-claude-code/SKILL.md +359 -0
- package/hooks/calidad-pre-commit.js +578 -0
- package/hooks/escaneo-secretos.js +302 -0
- package/hooks/extraccion-aprendizajes.js +550 -0
- package/hooks/linea-estado.js +249 -0
- package/hooks/monitor-contexto.js +230 -0
- package/hooks/proteccion-rutas.js +249 -0
- package/manifiestos/hooks-config.json +41 -0
- package/manifiestos/modulos.json +318 -0
- package/manifiestos/perfiles.json +189 -0
- package/package.json +45 -0
- package/plantillas/PROJECT.md +122 -0
- package/plantillas/REQUIREMENTS.md +132 -0
- package/plantillas/ROADMAP.md +143 -0
- package/plantillas/STATE.md +109 -0
- package/plantillas/research/ARCHITECTURE.md +220 -0
- package/plantillas/research/FEATURES.md +175 -0
- package/plantillas/research/PITFALLS.md +299 -0
- package/plantillas/research/STACK.md +233 -0
- package/plantillas/research/SUMMARY.md +165 -0
- package/plugin.json +144 -0
- package/reglas/accesibilidad.md +269 -0
- package/reglas/api-diseno.md +400 -0
- package/reglas/arquitectura.md +183 -0
- package/reglas/cloud-infra.md +247 -0
- package/reglas/docs.md +245 -0
- package/reglas/estilo-codigo.md +179 -0
- package/reglas/git-workflow.md +186 -0
- package/reglas/performance.md +195 -0
- package/reglas/pruebas.md +159 -0
- package/reglas/seguridad.md +151 -0
- package/reglas/skills-estandar.md +473 -0
- package/scripts/actualizar.js +51 -0
- package/scripts/desinstalar.js +86 -0
- package/scripts/doctor.js +222 -0
- package/scripts/inicializar.js +89 -0
- package/scripts/instalador.js +333 -0
- package/scripts/lib/detectar-runtime.js +177 -0
- package/scripts/lib/estado.js +112 -0
- package/scripts/lib/hooks-settings.js +283 -0
- package/scripts/lib/manifiestos.js +138 -0
- package/scripts/lib/seguridad.js +160 -0
- package/scripts/publicar.js +209 -0
- package/scripts/validar.js +120 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-avanzado
|
|
3
|
+
description: >
|
|
4
|
+
Frontend avanzado: Web Workers, Service Workers, PWA, WebSockets, SSE, IndexedDB,
|
|
5
|
+
Web Components, Shadow DOM, CSS Container Queries, CSS Layers, View Transitions API.
|
|
6
|
+
Patrones de optimización de rendimiento.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Frontend Avanzado — APIs Nativas y Rendimiento
|
|
10
|
+
|
|
11
|
+
## Por qué importan las APIs nativas del navegador
|
|
12
|
+
|
|
13
|
+
Los frameworks abstraen muchas APIs del navegador, pero conocerlas directamente permite:
|
|
14
|
+
- Descargar trabajo pesado del hilo principal (Web Workers).
|
|
15
|
+
- Ofrecer experiencias offline (Service Workers + IndexedDB).
|
|
16
|
+
- Actualización en tiempo real sin polling (WebSockets/SSE).
|
|
17
|
+
- Estilos que responden al contenedor, no sólo a la pantalla (Container Queries).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Web Workers — Cómputo en Hilo Separado
|
|
22
|
+
|
|
23
|
+
### MAL: cálculo pesado en el hilo principal
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// MAL — bloquea el hilo principal, congela la UI
|
|
27
|
+
function calcularEstadisticas(datos: number[]): EstadisticasResultado {
|
|
28
|
+
// Operación O(n²) — bloquea la UI durante segundos
|
|
29
|
+
const correlaciones = datos.flatMap((a, i) =>
|
|
30
|
+
datos.slice(i + 1).map(b => calcularCorrelacion(a, b))
|
|
31
|
+
);
|
|
32
|
+
return { correlaciones, promedio: datos.reduce((s, n) => s + n, 0) / datos.length };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// En el componente — BLOQUEA renderizado
|
|
36
|
+
const stats = calcularEstadisticas(misMuchosDatos); // ❌ Freeze de UI
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### BIEN: Worker con comunicación tipada
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// estadisticas.worker.ts
|
|
43
|
+
/// <reference lib="webworker" />
|
|
44
|
+
|
|
45
|
+
interface MensajeEntrada {
|
|
46
|
+
tipo: "CALCULAR";
|
|
47
|
+
datos: number[];
|
|
48
|
+
id: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface MensajeSalida {
|
|
52
|
+
tipo: "RESULTADO" | "ERROR" | "PROGRESO";
|
|
53
|
+
id: string;
|
|
54
|
+
payload: EstadisticasResultado | string | number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
self.addEventListener("message", (event: MessageEvent<MensajeEntrada>) => {
|
|
58
|
+
const { tipo, datos, id } = event.data;
|
|
59
|
+
|
|
60
|
+
if (tipo === "CALCULAR") {
|
|
61
|
+
try {
|
|
62
|
+
// Reportar progreso cada 10%
|
|
63
|
+
const total = datos.length;
|
|
64
|
+
const correlaciones: number[] = [];
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < total; i++) {
|
|
67
|
+
for (let j = i + 1; j < total; j++) {
|
|
68
|
+
correlaciones.push(calcularCorrelacion(datos[i], datos[j]));
|
|
69
|
+
}
|
|
70
|
+
if (i % Math.floor(total / 10) === 0) {
|
|
71
|
+
self.postMessage({
|
|
72
|
+
tipo: "PROGRESO",
|
|
73
|
+
id,
|
|
74
|
+
payload: Math.round((i / total) * 100),
|
|
75
|
+
} satisfies MensajeSalida);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
self.postMessage({
|
|
80
|
+
tipo: "RESULTADO",
|
|
81
|
+
id,
|
|
82
|
+
payload: { correlaciones, promedio: datos.reduce((s, n) => s + n, 0) / total },
|
|
83
|
+
} satisfies MensajeSalida);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
self.postMessage({ tipo: "ERROR", id, payload: String(e) } satisfies MensajeSalida);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// servicio-worker.service.ts (Angular)
|
|
91
|
+
@Injectable({ providedIn: "root" })
|
|
92
|
+
export class ServicioEstadisticas {
|
|
93
|
+
private worker = new Worker(
|
|
94
|
+
new URL("../workers/estadisticas.worker", import.meta.url),
|
|
95
|
+
{ type: "module" }
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
calcular(datos: number[]): Observable<EstadisticasResultado> {
|
|
99
|
+
const id = crypto.randomUUID();
|
|
100
|
+
|
|
101
|
+
return new Observable(observer => {
|
|
102
|
+
const manejador = (event: MessageEvent<MensajeSalida>) => {
|
|
103
|
+
if (event.data.id !== id) return;
|
|
104
|
+
|
|
105
|
+
switch (event.data.tipo) {
|
|
106
|
+
case "PROGRESO":
|
|
107
|
+
// Emitir progreso si se requiere
|
|
108
|
+
break;
|
|
109
|
+
case "RESULTADO":
|
|
110
|
+
observer.next(event.data.payload as EstadisticasResultado);
|
|
111
|
+
observer.complete();
|
|
112
|
+
this.worker.removeEventListener("message", manejador);
|
|
113
|
+
break;
|
|
114
|
+
case "ERROR":
|
|
115
|
+
observer.error(new Error(event.data.payload as string));
|
|
116
|
+
this.worker.removeEventListener("message", manejador);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.worker.addEventListener("message", manejador);
|
|
122
|
+
this.worker.postMessage({ tipo: "CALCULAR", datos, id });
|
|
123
|
+
|
|
124
|
+
return () => this.worker.removeEventListener("message", manejador);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Service Workers — Estrategias de Caché
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// service-worker.ts
|
|
136
|
+
const CACHE_VERSION = "v3";
|
|
137
|
+
const CACHE_ESTATICO = `estatico-${CACHE_VERSION}`;
|
|
138
|
+
const CACHE_DINAMICO = `dinamico-${CACHE_VERSION}`;
|
|
139
|
+
|
|
140
|
+
const RECURSOS_PRECACHEADOS = [
|
|
141
|
+
"/",
|
|
142
|
+
"/manifest.json",
|
|
143
|
+
"/offline.html",
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
// Instalación — precachear recursos estáticos
|
|
147
|
+
self.addEventListener("install", (event: ExtendableEvent) => {
|
|
148
|
+
event.waitUntil(
|
|
149
|
+
caches.open(CACHE_ESTATICO).then(cache => cache.addAll(RECURSOS_PRECACHEADOS))
|
|
150
|
+
);
|
|
151
|
+
(self as ServiceWorkerGlobalScope).skipWaiting();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Activación — limpiar cachés viejos
|
|
155
|
+
self.addEventListener("activate", (event: ExtendableEvent) => {
|
|
156
|
+
event.waitUntil(
|
|
157
|
+
caches.keys().then(claves =>
|
|
158
|
+
Promise.all(
|
|
159
|
+
claves
|
|
160
|
+
.filter(c => c !== CACHE_ESTATICO && c !== CACHE_DINAMICO)
|
|
161
|
+
.map(c => caches.delete(c))
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
(self as ServiceWorkerGlobalScope).clients.claim();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Estrategias de fetch
|
|
169
|
+
self.addEventListener("fetch", (event: FetchEvent) => {
|
|
170
|
+
const { request } = event;
|
|
171
|
+
const url = new URL(request.url);
|
|
172
|
+
|
|
173
|
+
// API calls — Network First (con fallback a caché)
|
|
174
|
+
if (url.pathname.startsWith("/api/")) {
|
|
175
|
+
event.respondWith(networkFirst(request));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Recursos estáticos — Cache First
|
|
180
|
+
if (request.destination === "image" || url.pathname.endsWith(".js")) {
|
|
181
|
+
event.respondWith(cacheFirst(request));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// HTML — Stale While Revalidate
|
|
186
|
+
event.respondWith(staleWhileRevalidate(request));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
async function networkFirst(request: Request): Promise<Response> {
|
|
190
|
+
try {
|
|
191
|
+
const respuesta = await fetch(request.clone());
|
|
192
|
+
const cache = await caches.open(CACHE_DINAMICO);
|
|
193
|
+
cache.put(request, respuesta.clone());
|
|
194
|
+
return respuesta;
|
|
195
|
+
} catch {
|
|
196
|
+
const cached = await caches.match(request);
|
|
197
|
+
return cached ?? caches.match("/offline.html") as Promise<Response>;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function cacheFirst(request: Request): Promise<Response> {
|
|
202
|
+
const cached = await caches.match(request);
|
|
203
|
+
if (cached) return cached;
|
|
204
|
+
|
|
205
|
+
const respuesta = await fetch(request);
|
|
206
|
+
const cache = await caches.open(CACHE_DINAMICO);
|
|
207
|
+
cache.put(request, respuesta.clone());
|
|
208
|
+
return respuesta;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function staleWhileRevalidate(request: Request): Promise<Response> {
|
|
212
|
+
const cache = await caches.open(CACHE_DINAMICO);
|
|
213
|
+
const cached = await caches.match(request);
|
|
214
|
+
|
|
215
|
+
const fetchPromise = fetch(request).then(respuesta => {
|
|
216
|
+
cache.put(request, respuesta.clone());
|
|
217
|
+
return respuesta;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return cached ?? fetchPromise;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## WebSockets — Conexión Robusta con Reconexión
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// websocket.service.ts
|
|
230
|
+
@Injectable({ providedIn: "root" })
|
|
231
|
+
export class WebSocketService {
|
|
232
|
+
private socket: WebSocket | null = null;
|
|
233
|
+
private reintentos = 0;
|
|
234
|
+
private readonly MAX_REINTENTOS = 5;
|
|
235
|
+
private readonly DELAY_BASE_MS = 1000;
|
|
236
|
+
|
|
237
|
+
private readonly _mensajes$ = new Subject<MensajeWS>();
|
|
238
|
+
private readonly _estado$ = new BehaviorSubject<"conectando" | "conectado" | "desconectado">("desconectado");
|
|
239
|
+
|
|
240
|
+
readonly mensajes$ = this._mensajes$.asObservable();
|
|
241
|
+
readonly estado$ = this._estado$.asObservable();
|
|
242
|
+
|
|
243
|
+
conectar(url: string): void {
|
|
244
|
+
this._estado$.next("conectando");
|
|
245
|
+
this.socket = new WebSocket(url);
|
|
246
|
+
|
|
247
|
+
this.socket.onopen = () => {
|
|
248
|
+
this.reintentos = 0;
|
|
249
|
+
this._estado$.next("conectado");
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
this.socket.onmessage = (event: MessageEvent) => {
|
|
253
|
+
try {
|
|
254
|
+
const mensaje = JSON.parse(event.data) as MensajeWS;
|
|
255
|
+
this._mensajes$.next(mensaje);
|
|
256
|
+
} catch {
|
|
257
|
+
console.warn("Mensaje WS no parseado:", event.data);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
this.socket.onclose = (event: CloseEvent) => {
|
|
262
|
+
this._estado$.next("desconectado");
|
|
263
|
+
|
|
264
|
+
// No reconectar si fue cierre limpio
|
|
265
|
+
if (event.wasClean || this.reintentos >= this.MAX_REINTENTOS) return;
|
|
266
|
+
|
|
267
|
+
const delay = this.DELAY_BASE_MS * 2 ** this.reintentos;
|
|
268
|
+
this.reintentos++;
|
|
269
|
+
|
|
270
|
+
setTimeout(() => this.conectar(url), delay);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
this.socket.onerror = () => {
|
|
274
|
+
// onclose se llamará después del error
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
enviar<T>(tipo: string, payload: T): void {
|
|
279
|
+
if (this.socket?.readyState !== WebSocket.OPEN) {
|
|
280
|
+
throw new Error("WebSocket no conectado");
|
|
281
|
+
}
|
|
282
|
+
this.socket.send(JSON.stringify({ tipo, payload, timestamp: Date.now() }));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
desconectar(): void {
|
|
286
|
+
this.reintentos = this.MAX_REINTENTOS; // Prevenir reconexión automática
|
|
287
|
+
this.socket?.close(1000, "Cierre normal");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Server-Sent Events — Para Flujos Unidireccionales
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// sse.service.ts — más simple que WS para flujos del servidor al cliente
|
|
298
|
+
@Injectable({ providedIn: "root" })
|
|
299
|
+
export class SSEService {
|
|
300
|
+
|
|
301
|
+
escuchar<T>(url: string): Observable<T> {
|
|
302
|
+
return new Observable<T>(observer => {
|
|
303
|
+
const fuente = new EventSource(url, { withCredentials: true });
|
|
304
|
+
|
|
305
|
+
fuente.onmessage = (event: MessageEvent) => {
|
|
306
|
+
try {
|
|
307
|
+
observer.next(JSON.parse(event.data) as T);
|
|
308
|
+
} catch {
|
|
309
|
+
observer.error(new Error(`SSE: JSON inválido: ${event.data}`));
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
fuente.onerror = () => {
|
|
314
|
+
observer.error(new Error("Conexión SSE perdida"));
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Cleanup al desuscribirse
|
|
318
|
+
return () => fuente.close();
|
|
319
|
+
}).pipe(
|
|
320
|
+
// Reconexión automática con delay
|
|
321
|
+
retryWhen(errores =>
|
|
322
|
+
errores.pipe(
|
|
323
|
+
delayWhen((_, intento) => timer(Math.min(1000 * 2 ** intento, 30000)))
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Uso en componente
|
|
331
|
+
export class NotificacionesComponent {
|
|
332
|
+
private sseService = inject(SSEService);
|
|
333
|
+
private destroy$ = new Subject<void>();
|
|
334
|
+
|
|
335
|
+
readonly notificaciones = signal<Notificacion[]>([]);
|
|
336
|
+
|
|
337
|
+
ngOnInit(): void {
|
|
338
|
+
this.sseService
|
|
339
|
+
.escuchar<Notificacion>("/api/notificaciones/stream")
|
|
340
|
+
.pipe(takeUntil(this.destroy$))
|
|
341
|
+
.subscribe(notificacion => {
|
|
342
|
+
this.notificaciones.update(lista => [notificacion, ...lista].slice(0, 50));
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
ngOnDestroy(): void {
|
|
347
|
+
this.destroy$.next();
|
|
348
|
+
this.destroy$.complete();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## IndexedDB — Almacenamiento Estructurado Offline
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// idb.service.ts — usando la librería `idb` para API promisificada
|
|
359
|
+
import { openDB, DBSchema, IDBPDatabase } from "idb";
|
|
360
|
+
|
|
361
|
+
interface EsquemaBD extends DBSchema {
|
|
362
|
+
pedidos: {
|
|
363
|
+
key: string;
|
|
364
|
+
value: { id: string; datos: object; sincronizado: boolean; timestamp: number };
|
|
365
|
+
indexes: { "por-sincronizado": boolean };
|
|
366
|
+
};
|
|
367
|
+
cache_respuestas: {
|
|
368
|
+
key: string; // URL
|
|
369
|
+
value: { url: string; datos: unknown; expira: number };
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@Injectable({ providedIn: "root" })
|
|
374
|
+
export class AlmacenamientoLocalService {
|
|
375
|
+
private db!: IDBPDatabase<EsquemaBD>;
|
|
376
|
+
|
|
377
|
+
async inicializar(): Promise<void> {
|
|
378
|
+
this.db = await openDB<EsquemaBD>("mi-app-db", 2, {
|
|
379
|
+
upgrade(db, versionAnterior) {
|
|
380
|
+
if (versionAnterior < 1) {
|
|
381
|
+
const storePedidos = db.createObjectStore("pedidos", { keyPath: "id" });
|
|
382
|
+
storePedidos.createIndex("por-sincronizado", "sincronizado");
|
|
383
|
+
db.createObjectStore("cache_respuestas", { keyPath: "url" });
|
|
384
|
+
}
|
|
385
|
+
if (versionAnterior < 2) {
|
|
386
|
+
// Migraciones incrementales por versión
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async guardarPedidoOffline(pedido: object & { id: string }): Promise<void> {
|
|
393
|
+
await this.db.put("pedidos", {
|
|
394
|
+
id: pedido.id,
|
|
395
|
+
datos: pedido,
|
|
396
|
+
sincronizado: false,
|
|
397
|
+
timestamp: Date.now(),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async sincronizarPendientes(): Promise<void> {
|
|
402
|
+
const pendientes = await this.db.getAllFromIndex("pedidos", "por-sincronizado", false);
|
|
403
|
+
|
|
404
|
+
for (const pendiente of pendientes) {
|
|
405
|
+
try {
|
|
406
|
+
await fetch("/api/pedidos", {
|
|
407
|
+
method: "POST",
|
|
408
|
+
body: JSON.stringify(pendiente.datos),
|
|
409
|
+
headers: { "Content-Type": "application/json" },
|
|
410
|
+
});
|
|
411
|
+
await this.db.put("pedidos", { ...pendiente, sincronizado: true });
|
|
412
|
+
} catch {
|
|
413
|
+
// Sin red — se reintentará en la próxima sesión
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## CSS Container Queries
|
|
423
|
+
|
|
424
|
+
```css
|
|
425
|
+
/* ANTES: media queries globales — el componente depende del viewport */
|
|
426
|
+
@media (max-width: 768px) {
|
|
427
|
+
.tarjeta-producto { flex-direction: column; }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* AHORA: container queries — el componente responde a su contenedor */
|
|
431
|
+
.contenedor-tarjetas {
|
|
432
|
+
container-type: inline-size;
|
|
433
|
+
container-name: tarjetas;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* El componente es reutilizable en cualquier contexto */
|
|
437
|
+
@container tarjetas (min-width: 400px) {
|
|
438
|
+
.tarjeta-producto {
|
|
439
|
+
display: flex;
|
|
440
|
+
flex-direction: row;
|
|
441
|
+
gap: 1rem;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
@container tarjetas (max-width: 399px) {
|
|
446
|
+
.tarjeta-producto {
|
|
447
|
+
display: flex;
|
|
448
|
+
flex-direction: column;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* Container queries con unidades de contenedor */
|
|
453
|
+
.titulo-tarjeta {
|
|
454
|
+
font-size: clamp(0.875rem, 4cqi, 1.25rem); /* cqi = % del ancho del contenedor */
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## CSS Cascade Layers
|
|
461
|
+
|
|
462
|
+
```css
|
|
463
|
+
/* Orden de capas: menor a mayor especificidad */
|
|
464
|
+
@layer reset, base, componentes, utilidades, sobreescrituras;
|
|
465
|
+
|
|
466
|
+
@layer reset {
|
|
467
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@layer base {
|
|
471
|
+
body { font-family: system-ui, sans-serif; line-height: 1.5; }
|
|
472
|
+
h1, h2, h3 { line-height: 1.2; }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@layer componentes {
|
|
476
|
+
.btn {
|
|
477
|
+
padding: 0.5rem 1rem;
|
|
478
|
+
border-radius: 0.25rem;
|
|
479
|
+
cursor: pointer;
|
|
480
|
+
}
|
|
481
|
+
.btn-primario { background: var(--color-primario); color: white; }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@layer utilidades {
|
|
485
|
+
/* Las utilidades siempre ganan sobre componentes */
|
|
486
|
+
.mt-4 { margin-top: 1rem; }
|
|
487
|
+
.hidden { display: none; }
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
@layer sobreescrituras {
|
|
491
|
+
/* Personalizaciones específicas de tema/cliente */
|
|
492
|
+
.btn-primario { border-radius: 9999px; } /* Botones pill para cliente X */
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## View Transitions API
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// view-transitions.service.ts
|
|
502
|
+
@Injectable({ providedIn: "root" })
|
|
503
|
+
export class TransicionesService {
|
|
504
|
+
private router = inject(Router);
|
|
505
|
+
|
|
506
|
+
inicializarTransiciones(): void {
|
|
507
|
+
if (!("startViewTransition" in document)) return; // Fallback
|
|
508
|
+
|
|
509
|
+
this.router.events
|
|
510
|
+
.pipe(filter(e => e instanceof NavigationStart))
|
|
511
|
+
.subscribe(() => {
|
|
512
|
+
(document as any).startViewTransition(() =>
|
|
513
|
+
// El router navega dentro de la transición
|
|
514
|
+
new Promise<void>(resolve => {
|
|
515
|
+
const sub = this.router.events
|
|
516
|
+
.pipe(filter(e => e instanceof NavigationEnd), take(1))
|
|
517
|
+
.subscribe(() => { sub.unsubscribe(); resolve(); });
|
|
518
|
+
})
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
```css
|
|
526
|
+
/* Transición de vista personalizada */
|
|
527
|
+
::view-transition-old(root) {
|
|
528
|
+
animation: desvanece-sale 0.3s ease-out;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
::view-transition-new(root) {
|
|
532
|
+
animation: desvanece-entra 0.3s ease-in;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* Transición específica para elemento */
|
|
536
|
+
.tarjeta-detalle {
|
|
537
|
+
view-transition-name: tarjeta-activa;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
@keyframes desvanece-sale { to { opacity: 0; transform: translateX(-20px); } }
|
|
541
|
+
@keyframes desvanece-entra { from { opacity: 0; transform: translateX( 20px); } }
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Checklist de Revisión — Frontend Avanzado
|
|
547
|
+
|
|
548
|
+
- [ ] Cálculos que toman >50ms están en un Web Worker, no en el hilo principal.
|
|
549
|
+
- [ ] El Service Worker implementa la estrategia correcta por tipo de recurso (Cache First, Network First, SWR).
|
|
550
|
+
- [ ] IndexedDB tiene esquema versionado con migraciones incrementales.
|
|
551
|
+
- [ ] Las conexiones WebSocket/SSE tienen reconexión automática con backoff exponencial.
|
|
552
|
+
- [ ] Los Container Queries se usan en lugar de media queries para componentes reutilizables.
|
|
553
|
+
- [ ] CSS Layers están definidas con orden explícito (`@layer reset, base, componentes, utilidades`).
|
|
554
|
+
- [ ] View Transitions tiene fallback para navegadores sin soporte.
|
|
555
|
+
- [ ] El Service Worker se actualiza correctamente al publicar nuevas versiones (`skipWaiting` + `clients.claim`).
|