slicejs-web-framework 3.4.4 → 3.5.1
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/Slice/Components/Structural/Controller/Controller.js +135 -0
- package/Slice/Components/Structural/EventManager/EventManager.js +289 -4
- package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +368 -30
- package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -0
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +56 -0
- package/Slice/Slice.js +14 -1
- package/Slice/tests/event-emitters.test.js +79 -0
- package/Slice/tests/event-registry.test.js +141 -0
- package/Slice/tests/leak-orphans.test.js +131 -0
- package/api/{middleware → framework}/securityMiddleware.js +18 -74
- package/api/framework/server.js +333 -0
- package/api/index.js +5 -279
- package/api/tests/public-env-resolver.test.js +1 -1
- package/package.json +4 -2
- package/sliceConfig.schema.json +10 -1
- package/src/App/index.html +31 -1
- package/src/manifest.json +15 -0
- package/src/robots.txt +4 -0
- package/src/sitemap.xml +8 -0
- package/src/sliceConfig.json +5 -1
- package/types/index.d.ts +12 -0
- package/slicejs-cli/.github/workflows/ci.yml +0 -43
- package/slicejs-cli/LICENSE +0 -21
- package/slicejs-cli/assets/Slice.js-logo.png +0 -0
- package/slicejs-cli/client.js +0 -740
- package/slicejs-cli/commands/Print.js +0 -164
- package/slicejs-cli/commands/Validations.js +0 -92
- package/slicejs-cli/commands/build/build.js +0 -40
- package/slicejs-cli/commands/buildProduction/buildProduction.js +0 -577
- package/slicejs-cli/commands/bundle/bundle.js +0 -236
- package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +0 -55
- package/slicejs-cli/commands/createComponent/createComponent.js +0 -128
- package/slicejs-cli/commands/deleteComponent/deleteComponent.js +0 -81
- package/slicejs-cli/commands/doctor/doctor.js +0 -516
- package/slicejs-cli/commands/getComponent/getComponent.js +0 -695
- package/slicejs-cli/commands/init/init.js +0 -473
- package/slicejs-cli/commands/listComponents/listComponents.js +0 -172
- package/slicejs-cli/commands/startServer/startServer.js +0 -240
- package/slicejs-cli/commands/startServer/watchServer.js +0 -66
- package/slicejs-cli/commands/types/types.js +0 -583
- package/slicejs-cli/commands/utils/LocalCliDelegation.js +0 -53
- package/slicejs-cli/commands/utils/PackageManager.js +0 -148
- package/slicejs-cli/commands/utils/PathHelper.js +0 -75
- package/slicejs-cli/commands/utils/VersionChecker.js +0 -169
- package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +0 -2525
- package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +0 -933
- package/slicejs-cli/commands/utils/loadConfig.js +0 -31
- package/slicejs-cli/commands/utils/sliceScripts.js +0 -48
- package/slicejs-cli/commands/utils/updateManager.js +0 -471
- package/slicejs-cli/package.json +0 -71
- package/slicejs-cli/playwright.config.js +0 -51
- package/slicejs-cli/pnpm-lock.yaml +0 -1629
- package/slicejs-cli/tests/build-command-integration.test.js +0 -87
- package/slicejs-cli/tests/build-mangling-e2e.test.js +0 -290
- package/slicejs-cli/tests/build-production-e2e.test.js +0 -140
- package/slicejs-cli/tests/builder-edge-cases.test.js +0 -322
- package/slicejs-cli/tests/bundle-generate-e2e.test.js +0 -115
- package/slicejs-cli/tests/bundle-generator.test.js +0 -691
- package/slicejs-cli/tests/bundle-v2-register-output.test.js +0 -470
- package/slicejs-cli/tests/bundling-dependency-edges.test.js +0 -127
- package/slicejs-cli/tests/bundling-imports-unit.test.js +0 -267
- package/slicejs-cli/tests/client-launcher-contract.test.js +0 -211
- package/slicejs-cli/tests/commands-component-crud.test.js +0 -128
- package/slicejs-cli/tests/commands-doctor.test.js +0 -152
- package/slicejs-cli/tests/commands-version-checker.test.js +0 -37
- package/slicejs-cli/tests/component-registry-parse.test.js +0 -34
- package/slicejs-cli/tests/dependency-analyzer.test.js +0 -24
- package/slicejs-cli/tests/e2e/bundles.spec.js +0 -91
- package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +0 -56
- package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
- package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
- package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
- package/slicejs-cli/tests/e2e/fixtures/components/registry.json +0 -12
- package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +0 -65
- package/slicejs-cli/tests/e2e/navigation.spec.js +0 -44
- package/slicejs-cli/tests/e2e/render.spec.js +0 -34
- package/slicejs-cli/tests/e2e/serve.mjs +0 -264
- package/slicejs-cli/tests/e2e/shared-deps.spec.js +0 -61
- package/slicejs-cli/tests/e2e/unminified.spec.js +0 -33
- package/slicejs-cli/tests/e2e-serve.test.js +0 -148
- package/slicejs-cli/tests/fixtures/components.js +0 -8
- package/slicejs-cli/tests/fixtures/sliceConfig.json +0 -74
- package/slicejs-cli/tests/getcomponent.test.js +0 -435
- package/slicejs-cli/tests/helpers/setup.js +0 -102
- package/slicejs-cli/tests/init-command-contract.test.js +0 -101
- package/slicejs-cli/tests/init-e2e.test.js +0 -82
- package/slicejs-cli/tests/init-project-isolation.test.js +0 -98
- package/slicejs-cli/tests/local-cli-delegation.test.js +0 -81
- package/slicejs-cli/tests/package-manager.test.js +0 -127
- package/slicejs-cli/tests/path-helper.test.js +0 -206
- package/slicejs-cli/tests/perf-budget.test.js +0 -84
- package/slicejs-cli/tests/postinstall-command.test.js +0 -103
- package/slicejs-cli/tests/property-mangling-regression.test.js +0 -182
- package/slicejs-cli/tests/types-breakage.test.js +0 -491
- package/slicejs-cli/tests/types-generator-errors.test.js +0 -361
- package/slicejs-cli/tests/types-generator.test.js +0 -348
- /package/api/{utils → framework}/publicEnvResolver.js +0 -0
|
@@ -26,6 +26,12 @@ export default class Controller {
|
|
|
26
26
|
this._pendingBuilds = new Map();
|
|
27
27
|
|
|
28
28
|
this.idCounter = 0;
|
|
29
|
+
|
|
30
|
+
// 🔍 Leak detection (dev-only). App-provided predicates that mark a
|
|
31
|
+
// detached-but-registered component as intentional (not a leak).
|
|
32
|
+
this._leakExclusions = [];
|
|
33
|
+
// Ring buffer of activeComponents.size samples for the growth heuristic.
|
|
34
|
+
this._growthSamples = [];
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/**
|
|
@@ -1268,4 +1274,133 @@ export default class Controller {
|
|
|
1268
1274
|
|
|
1269
1275
|
return count;
|
|
1270
1276
|
}
|
|
1277
|
+
|
|
1278
|
+
// ============================================================
|
|
1279
|
+
// 🔍 LEAK DETECTION (dev-only)
|
|
1280
|
+
// A component is a candidate leak when it stays registered in
|
|
1281
|
+
// activeComponents while detached from the live DOM and was not
|
|
1282
|
+
// intentionally cached (Route/MultiRoute/Router). Read-only.
|
|
1283
|
+
// ============================================================
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Register a predicate that excludes a detached component from being flagged
|
|
1287
|
+
* as a leak (e.g. caches that intentionally keep instances off-DOM).
|
|
1288
|
+
* @param {(component: HTMLElement) => boolean} predicate
|
|
1289
|
+
*/
|
|
1290
|
+
registerLeakExclusion(predicate) {
|
|
1291
|
+
if (typeof predicate === 'function') {
|
|
1292
|
+
this._leakExclusions.push(predicate);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Is this detached component intentionally retained (not a leak)?
|
|
1298
|
+
* @param {HTMLElement} component
|
|
1299
|
+
* @returns {boolean}
|
|
1300
|
+
* @private
|
|
1301
|
+
*/
|
|
1302
|
+
_isExcludedOrphan(component) {
|
|
1303
|
+
// (1) Cache-owner marker — Route/MultiRoute flag their cached instances.
|
|
1304
|
+
if (component.__sliceCached === true) return true;
|
|
1305
|
+
// (2) Router-managed instances are reused across navigation by sliceId.
|
|
1306
|
+
if (typeof component.sliceId === 'string' && /^route-/.test(component.sliceId)) return true;
|
|
1307
|
+
// (3) App-provided predicates.
|
|
1308
|
+
for (const predicate of this._leakExclusions) {
|
|
1309
|
+
try {
|
|
1310
|
+
if (predicate(component)) return true;
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
slice.logger.logError('Controller', 'Leak exclusion predicate threw', error);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Does an ancestor explain this component's detachment? If so we report only
|
|
1320
|
+
* the detached root, not every descendant of a removed/cached subtree.
|
|
1321
|
+
* @param {HTMLElement} component
|
|
1322
|
+
* @param {Set<string>} detachedIds
|
|
1323
|
+
* @returns {boolean}
|
|
1324
|
+
* @private
|
|
1325
|
+
*/
|
|
1326
|
+
_hasDetachedAncestor(component, detachedIds) {
|
|
1327
|
+
let parent = component.parentComponent;
|
|
1328
|
+
while (parent) {
|
|
1329
|
+
if ((parent.sliceId && detachedIds.has(parent.sliceId)) || this._isExcludedOrphan(parent)) {
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
parent = parent.parentComponent;
|
|
1333
|
+
}
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Build a human-readable retain path (component → parent → …) for diagnosis.
|
|
1339
|
+
* @param {HTMLElement} component
|
|
1340
|
+
* @returns {string[]}
|
|
1341
|
+
* @private
|
|
1342
|
+
*/
|
|
1343
|
+
_retainPath(component) {
|
|
1344
|
+
const path = [];
|
|
1345
|
+
let node = component;
|
|
1346
|
+
let guard = 0;
|
|
1347
|
+
while (node && guard++ < 50) {
|
|
1348
|
+
path.push(`${node.constructor?.name || 'Unknown'}(${node.sliceId || '?'})`);
|
|
1349
|
+
node = node.parentComponent;
|
|
1350
|
+
}
|
|
1351
|
+
return path;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Find candidate leaks: components registered in activeComponents but detached
|
|
1356
|
+
* from the live DOM and not intentionally cached. Pure read-only.
|
|
1357
|
+
* @returns {Array<{ sliceId: string, name: string, reason: string, retainPath: string[] }>}
|
|
1358
|
+
*/
|
|
1359
|
+
findOrphans() {
|
|
1360
|
+
const detached = [];
|
|
1361
|
+
for (const [sliceId, component] of this.activeComponents) {
|
|
1362
|
+
if (component instanceof HTMLElement && !component.isConnected) {
|
|
1363
|
+
detached.push([sliceId, component]);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const detachedIds = new Set(detached.map(([sliceId]) => sliceId));
|
|
1368
|
+
const orphans = [];
|
|
1369
|
+
|
|
1370
|
+
for (const [sliceId, component] of detached) {
|
|
1371
|
+
if (this._isExcludedOrphan(component)) continue;
|
|
1372
|
+
if (this._hasDetachedAncestor(component, detachedIds)) continue;
|
|
1373
|
+
orphans.push({
|
|
1374
|
+
sliceId,
|
|
1375
|
+
name: component.constructor?.name || 'Unknown',
|
|
1376
|
+
reason: 'registered but detached from DOM without destroy',
|
|
1377
|
+
retainPath: this._retainPath(component)
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return orphans;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Record an activeComponents.size sample for the monotonic-growth heuristic.
|
|
1386
|
+
* Called by the Leak Inspector on each refresh while it is open.
|
|
1387
|
+
*/
|
|
1388
|
+
sampleComponentGrowth() {
|
|
1389
|
+
this._growthSamples.push(this.activeComponents.size);
|
|
1390
|
+
if (this._growthSamples.length > 20) this._growthSamples.shift();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Heuristic: has the active component count grown without ever shrinking
|
|
1395
|
+
* across the sampled window? Suggests an accumulation leak.
|
|
1396
|
+
* @returns {boolean}
|
|
1397
|
+
*/
|
|
1398
|
+
isGrowthMonotonic() {
|
|
1399
|
+
const samples = this._growthSamples;
|
|
1400
|
+
if (samples.length < 5) return false;
|
|
1401
|
+
for (let i = 1; i < samples.length; i++) {
|
|
1402
|
+
if (samples[i] < samples[i - 1]) return false;
|
|
1403
|
+
}
|
|
1404
|
+
return samples[samples.length - 1] > samples[0];
|
|
1405
|
+
}
|
|
1271
1406
|
}
|
|
@@ -29,17 +29,224 @@ export default class EventManager {
|
|
|
29
29
|
// Contador de emits por evento en la sesión actual de grabación
|
|
30
30
|
this.emitCounts = new Map();
|
|
31
31
|
|
|
32
|
+
// Atribución de emisores por evento (tracing): Map<eventName, Map<key, { sliceId?, name?, source?, count, lastTs }>>
|
|
33
|
+
// Se llena solo mientras un debugger está abierto (igual que emitHistory).
|
|
34
|
+
this.emitters = new Map();
|
|
35
|
+
|
|
32
36
|
// Flag: solo grabamos cuando algún debugger UI está visible
|
|
33
37
|
this._recording = false;
|
|
34
38
|
|
|
35
39
|
// Contador para IDs únicos
|
|
36
40
|
this.idCounter = 0;
|
|
41
|
+
|
|
42
|
+
// ── Event registry (catálogo declarado vía register()) ──
|
|
43
|
+
// Map<eventName, { description, payload }>
|
|
44
|
+
this.registry = new Map();
|
|
45
|
+
// Validación activa: se enciende al primer register(). En loose mode (sin
|
|
46
|
+
// registry) NO se advierte nada — comportamiento idéntico al histórico.
|
|
47
|
+
this._validating = false;
|
|
48
|
+
// Warn-once: nombres ya advertidos como no declarados (evita spam).
|
|
49
|
+
this.undeclaredSeen = new Set();
|
|
50
|
+
// Drift para el debugger: eventos usados (emit/subscribe) pero no declarados.
|
|
51
|
+
this.undeclared = new Set();
|
|
52
|
+
// Warn-once: nombres ya advertidos por no tener namespace.
|
|
53
|
+
this._noNamespaceSeen = new Set();
|
|
54
|
+
|
|
55
|
+
// Grafo estático (documentación) generado por `slice types generate`:
|
|
56
|
+
// { events: { name: { payload, emitters[], listeners[] } }, dynamic: {...} }.
|
|
57
|
+
// Complementa el tracing observacional (this.emitters / this.emitHistory).
|
|
58
|
+
this.graph = null;
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
init() {
|
|
40
62
|
return true;
|
|
41
63
|
}
|
|
42
64
|
|
|
65
|
+
// ============================================
|
|
66
|
+
// REGISTRY - Catálogo de eventos declarados
|
|
67
|
+
// ============================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Declarar un catálogo de eventos. Mergeable: se puede llamar varias veces
|
|
71
|
+
* desde varios módulos. Enciende la validación dev (warn-on-undeclared).
|
|
72
|
+
*
|
|
73
|
+
* Dos formas:
|
|
74
|
+
* - register(catalog) con claves planas `'namespace:event'`.
|
|
75
|
+
* - register(namespace, catalog) que prefija cada clave con `namespace:`,
|
|
76
|
+
* para declarar un grupo sin repetir el prefijo.
|
|
77
|
+
*
|
|
78
|
+
* @param {string|Object<string, { description?: string, payload?: object|null }>} namespaceOrCatalog
|
|
79
|
+
* @param {Object<string, { description?: string, payload?: object|null }>} [maybeCatalog]
|
|
80
|
+
* @returns {EventManager} this (encadenable)
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* slice.events.register({
|
|
84
|
+
* 'user:login': { description: 'User logged in', payload: { id: 'number', name: 'string' } },
|
|
85
|
+
* 'cart:cleared': { description: 'Cart emptied', payload: null },
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // Agrupado por namespace
|
|
90
|
+
* slice.events.register('user', {
|
|
91
|
+
* login: { payload: { id: 'number', name: 'string' } },
|
|
92
|
+
* logout: { payload: null },
|
|
93
|
+
* }); // => 'user:login', 'user:logout'
|
|
94
|
+
*/
|
|
95
|
+
register(namespaceOrCatalog, maybeCatalog) {
|
|
96
|
+
let prefix = '';
|
|
97
|
+
let catalog = namespaceOrCatalog;
|
|
98
|
+
|
|
99
|
+
if (typeof namespaceOrCatalog === 'string') {
|
|
100
|
+
const ns = namespaceOrCatalog.trim();
|
|
101
|
+
prefix = ns.endsWith(':') ? ns : `${ns}:`;
|
|
102
|
+
catalog = maybeCatalog;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!catalog || typeof catalog !== 'object') {
|
|
106
|
+
slice.logger.logError('EventManager', 'register() requiere un objeto catálogo de eventos');
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Al primer register() sembramos los eventos internos del framework para
|
|
111
|
+
// que nunca disparen falsos warnings, y activamos la validación.
|
|
112
|
+
if (!this._validating) {
|
|
113
|
+
this._seedFrameworkEvents();
|
|
114
|
+
this._validating = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const [key, definition] of Object.entries(catalog)) {
|
|
118
|
+
const eventName = `${prefix}${key}`;
|
|
119
|
+
this._warnIfNoNamespace(eventName);
|
|
120
|
+
|
|
121
|
+
const entry = {
|
|
122
|
+
description: definition?.description || '',
|
|
123
|
+
payload: definition?.payload ?? null,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const existing = this.registry.get(eventName);
|
|
127
|
+
if (existing && JSON.stringify(existing.payload) !== JSON.stringify(entry.payload)) {
|
|
128
|
+
slice.logger.logWarning(
|
|
129
|
+
'EventManager',
|
|
130
|
+
`Evento "${eventName}" redeclarado con un payload distinto; se usa la última definición`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.registry.set(eventName, entry);
|
|
135
|
+
// Si estaba marcado como drift, ya no lo está.
|
|
136
|
+
this.undeclared.delete(eventName);
|
|
137
|
+
this.undeclaredSeen.delete(eventName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Cargar el grafo estático de eventos (manifest generado por el CLI). Es la capa
|
|
145
|
+
* de DOCUMENTACIÓN (emisores/oyentes por análisis de código, sin ejecutar), que
|
|
146
|
+
* complementa el tracing observacional en runtime.
|
|
147
|
+
* @param {{ events: object, dynamic?: object }} graph
|
|
148
|
+
* @returns {EventManager} this
|
|
149
|
+
*/
|
|
150
|
+
loadGraph(graph) {
|
|
151
|
+
if (graph && typeof graph === 'object' && graph.events) {
|
|
152
|
+
this.graph = graph;
|
|
153
|
+
}
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Emisores conocidos de un evento por análisis estático (documentación).
|
|
159
|
+
* @param {string} eventName
|
|
160
|
+
* @returns {Array<{ file: string, line: number, component: string|null }>}
|
|
161
|
+
*/
|
|
162
|
+
staticEmittersOf(eventName) {
|
|
163
|
+
return this.graph?.events?.[eventName]?.emitters || [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Oyentes conocidos de un evento por análisis estático (documentación).
|
|
168
|
+
* @param {string} eventName
|
|
169
|
+
* @returns {Array<{ file: string, line: number, component: string|null }>}
|
|
170
|
+
*/
|
|
171
|
+
staticListenersOf(eventName) {
|
|
172
|
+
return this.graph?.events?.[eventName]?.listeners || [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Namespace de un evento: el segmento antes del primer ':' (o null si no tiene).
|
|
177
|
+
* @param {string} eventName
|
|
178
|
+
* @returns {string|null}
|
|
179
|
+
*/
|
|
180
|
+
namespaceOf(eventName) {
|
|
181
|
+
const index = typeof eventName === 'string' ? eventName.indexOf(':') : -1;
|
|
182
|
+
return index > 0 ? eventName.slice(0, index) : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Sembrar los eventos internos del framework en el registry.
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_seedFrameworkEvents() {
|
|
190
|
+
const framework = {
|
|
191
|
+
'router:change': { description: 'Router navigated to a new route', payload: null },
|
|
192
|
+
'context:__created': { description: 'A context was created', payload: { name: 'string', state: 'object' } },
|
|
193
|
+
};
|
|
194
|
+
for (const [name, def] of Object.entries(framework)) {
|
|
195
|
+
if (!this.registry.has(name)) this.registry.set(name, def);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* ¿El evento está declarado en el registry?
|
|
201
|
+
* @param {string} eventName
|
|
202
|
+
* @returns {boolean}
|
|
203
|
+
*/
|
|
204
|
+
isDeclared(eventName) {
|
|
205
|
+
if (this.registry.has(eventName)) return true;
|
|
206
|
+
// Familia dinámica context:<name> emitida por el ContextManager.
|
|
207
|
+
if (eventName.startsWith('context:')) return true;
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Advertir (una sola vez, solo en dev) si el evento no está declarado.
|
|
213
|
+
* @private
|
|
214
|
+
*/
|
|
215
|
+
_warnIfUndeclared(eventName) {
|
|
216
|
+
if (!this._validating) return;
|
|
217
|
+
if (this.isDeclared(eventName)) return;
|
|
218
|
+
|
|
219
|
+
this.undeclared.add(eventName);
|
|
220
|
+
if (this.undeclaredSeen.has(eventName)) return;
|
|
221
|
+
this.undeclaredSeen.add(eventName);
|
|
222
|
+
|
|
223
|
+
if (!slice.isProduction || !slice.isProduction()) {
|
|
224
|
+
slice.logger.logWarning(
|
|
225
|
+
'EventManager',
|
|
226
|
+
`Undeclared event "${eventName}" — register it via slice.events.register({ '${eventName}': { payload: ... } })`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Advertir (una sola vez, solo en dev) si el evento no tiene namespace.
|
|
233
|
+
* Solo activo cuando hay un registry en uso (this._validating).
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
_warnIfNoNamespace(eventName) {
|
|
237
|
+
if (!this._validating) return;
|
|
238
|
+
if (typeof eventName !== 'string' || eventName.includes(':')) return;
|
|
239
|
+
if (this._noNamespaceSeen.has(eventName)) return;
|
|
240
|
+
this._noNamespaceSeen.add(eventName);
|
|
241
|
+
|
|
242
|
+
if (!slice.isProduction || !slice.isProduction()) {
|
|
243
|
+
slice.logger.logWarning(
|
|
244
|
+
'EventManager',
|
|
245
|
+
`Event "${eventName}" has no namespace — prefer "namespace:${eventName}" (e.g. group it with slice.events.register('app', { ${eventName}: ... })).`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
43
250
|
// ============================================
|
|
44
251
|
// API PRINCIPAL
|
|
45
252
|
// ============================================
|
|
@@ -68,6 +275,9 @@ export default class EventManager {
|
|
|
68
275
|
return null;
|
|
69
276
|
}
|
|
70
277
|
|
|
278
|
+
this._warnIfUndeclared(eventName);
|
|
279
|
+
this._warnIfNoNamespace(eventName);
|
|
280
|
+
|
|
71
281
|
const subscriptionId = `evt_${++this.idCounter}`;
|
|
72
282
|
|
|
73
283
|
// Crear Map para este evento si no existe
|
|
@@ -111,6 +321,7 @@ export default class EventManager {
|
|
|
111
321
|
this._recording = true;
|
|
112
322
|
this.emitHistory = [];
|
|
113
323
|
this.emitCounts = new Map();
|
|
324
|
+
this.emitters = new Map();
|
|
114
325
|
}
|
|
115
326
|
|
|
116
327
|
/**
|
|
@@ -126,6 +337,9 @@ export default class EventManager {
|
|
|
126
337
|
return null;
|
|
127
338
|
}
|
|
128
339
|
|
|
340
|
+
this._warnIfUndeclared(eventName);
|
|
341
|
+
this._warnIfNoNamespace(eventName);
|
|
342
|
+
|
|
129
343
|
const subscriptionId = `evt_${++this.idCounter}`;
|
|
130
344
|
|
|
131
345
|
if (!this.subscriptions.has(eventName)) {
|
|
@@ -188,13 +402,31 @@ export default class EventManager {
|
|
|
188
402
|
* slice.events.emit('cart:cleared'); // Sin datos
|
|
189
403
|
*/
|
|
190
404
|
emit(eventName, ...args) {
|
|
405
|
+
// Global (unbound) emit — emitter resolved best-effort from the call stack.
|
|
406
|
+
this._emitFrom(null, eventName, args);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Core emit. `emitter` is the known source ({ sliceId, name }) when emitted via
|
|
411
|
+
* a bound API, or null for global emits (then a dev stack-trace fallback runs).
|
|
412
|
+
* @param {{ sliceId?: string, name?: string }|null} emitter
|
|
413
|
+
* @param {string} eventName
|
|
414
|
+
* @param {any[]} args
|
|
415
|
+
* @private
|
|
416
|
+
*/
|
|
417
|
+
_emitFrom(emitter, eventName, args) {
|
|
191
418
|
slice.logger.info('EventManager', `Emitting "${eventName}"`, args[0] ?? null);
|
|
192
419
|
|
|
193
|
-
|
|
420
|
+
this._warnIfUndeclared(eventName);
|
|
421
|
+
this._warnIfNoNamespace(eventName);
|
|
422
|
+
|
|
423
|
+
// Solo grabamos el histórico (y la atribución de emisores) si algún debugger está abierto.
|
|
194
424
|
if (this._recording) {
|
|
195
|
-
|
|
425
|
+
const source = emitter || this._captureEmitterSource();
|
|
426
|
+
this.emitHistory.push({ eventName, timestamp: Date.now(), emitter: source });
|
|
196
427
|
if (this.emitHistory.length > 500) this.emitHistory.shift();
|
|
197
428
|
this.emitCounts.set(eventName, (this.emitCounts.get(eventName) || 0) + 1);
|
|
429
|
+
this._recordEmitter(eventName, source);
|
|
198
430
|
}
|
|
199
431
|
|
|
200
432
|
if (!this.subscriptions.has(eventName)) {
|
|
@@ -234,6 +466,47 @@ export default class EventManager {
|
|
|
234
466
|
}
|
|
235
467
|
}
|
|
236
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Aggregate an emitter under an event: who emits, with a count. Powers the
|
|
471
|
+
* "emitted by" tracing view in the debugger.
|
|
472
|
+
* @param {{ sliceId?: string, name?: string, source?: string }|null} emitter
|
|
473
|
+
* @private
|
|
474
|
+
*/
|
|
475
|
+
_recordEmitter(eventName, emitter) {
|
|
476
|
+
if (!emitter) return;
|
|
477
|
+
const key = emitter.sliceId || emitter.source || 'unknown';
|
|
478
|
+
if (!this.emitters.has(eventName)) this.emitters.set(eventName, new Map());
|
|
479
|
+
const bucket = this.emitters.get(eventName);
|
|
480
|
+
const existing = bucket.get(key);
|
|
481
|
+
if (existing) {
|
|
482
|
+
existing.count += 1;
|
|
483
|
+
existing.lastTs = Date.now();
|
|
484
|
+
} else {
|
|
485
|
+
bucket.set(key, { ...emitter, count: 1, lastTs: Date.now() });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Best-effort source of a global emit from the call stack (dev only). Returns
|
|
491
|
+
* { source: 'file.js:line' } or null. Robust: never throws, never runs in prod.
|
|
492
|
+
* @private
|
|
493
|
+
*/
|
|
494
|
+
_captureEmitterSource() {
|
|
495
|
+
if (slice.isProduction && slice.isProduction()) return null;
|
|
496
|
+
try {
|
|
497
|
+
const stack = new Error().stack || '';
|
|
498
|
+
for (const line of stack.split('\n')) {
|
|
499
|
+
// Skip the Error header and every frame inside this manager.
|
|
500
|
+
if (line.includes('EventManager.js')) continue;
|
|
501
|
+
const match = line.match(/([^/\\\s(]+\.js):(\d+):\d+/);
|
|
502
|
+
if (match) return { source: `${match[1]}:${match[2]}` };
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
/* tracing is best-effort */
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
237
510
|
// ============================================
|
|
238
511
|
// BIND - Vinculación a Componente
|
|
239
512
|
// ============================================
|
|
@@ -278,10 +551,22 @@ export default class EventManager {
|
|
|
278
551
|
},
|
|
279
552
|
|
|
280
553
|
/**
|
|
281
|
-
* Emitir un evento
|
|
554
|
+
* Emitir un evento (atribuido a este componente para el tracing).
|
|
282
555
|
*/
|
|
283
556
|
emit: (eventName, data) => {
|
|
284
|
-
self.
|
|
557
|
+
self._emitFrom(
|
|
558
|
+
{ sliceId: component.sliceId, name: component.constructor?.name || 'Component' },
|
|
559
|
+
eventName,
|
|
560
|
+
[data]
|
|
561
|
+
);
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Declarar eventos en el registry (passthrough a register()).
|
|
566
|
+
*/
|
|
567
|
+
register: (namespaceOrCatalog, maybeCatalog) => {
|
|
568
|
+
self.register(namespaceOrCatalog, maybeCatalog);
|
|
569
|
+
return self;
|
|
285
570
|
},
|
|
286
571
|
};
|
|
287
572
|
}
|