@everystate/perf 1.0.1 → 1.0.3

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/README.md CHANGED
@@ -59,6 +59,21 @@ store.set = function(path, value) {
59
59
 
60
60
  Fully reversible via `monitor.destroy()`.
61
61
 
62
+ ## Ecosystem
63
+
64
+ | Package | Description | License |
65
+ |---|---|---|
66
+ | [@everystate/aliases](https://www.npmjs.com/package/@everystate/aliases) | Ergonomic single-character and short-name DOM aliases for vanilla JS | MIT |
67
+ | [@everystate/core](https://www.npmjs.com/package/@everystate/core) | Path-based state management with wildcard subscriptions and async support. Core state engine (you are here). | MIT |
68
+ | [@everystate/css](https://www.npmjs.com/package/@everystate/css) | Reactive CSSOM engine: design tokens, typed validation, WCAG enforcement, all via path-based state | MIT |
69
+ | [@everystate/examples](https://www.npmjs.com/package/@everystate/examples) | Example applications and patterns | MIT |
70
+ | [@everystate/perf](https://www.npmjs.com/package/@everystate/perf) | Performance monitoring overlay | MIT |
71
+ | [@everystate/react](https://www.npmjs.com/package/@everystate/react) | React hooks adapter: `usePath`, `useIntent`, `useAsync` hooks and `EveryStateProvider` | MIT |
72
+ | [@everystate/renderer](https://www.npmjs.com/package/@everystate/renderer) | Direct-binding reactive renderer: `bind-*`, `set`, `each` attributes. Zero build step | Proprietary |
73
+ | [@everystate/router](https://www.npmjs.com/package/@everystate/router) | SPA routing as state | MIT |
74
+ | [@everystate/test](https://www.npmjs.com/package/@everystate/test) | Event-sequence testing for EveryState stores. Zero dependency. | Proprietary |
75
+ | [@everystate/view](https://www.npmjs.com/package/@everystate/view) | State-driven view: DOMless resolve + surgical DOM projector. View tree as first-class state | MIT |
76
+
62
77
  ## License
63
78
 
64
79
  MIT © Ajdin Imsirovic
package/index.d.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @everystate/perf
3
+ *
4
+ * Non-invasive performance monitoring for EveryState stores.
5
+ */
6
+
7
+ import type { EveryStateStore } from '@everystate/core';
8
+
9
+ export interface PerfMonitorOptions {
10
+ /** Maximum timeline events to keep (default: 10000) */
11
+ maxEvents?: number;
12
+ /** Track values in timeline events (default: false) */
13
+ trackValues?: boolean;
14
+ /** Track get() calls (default: false) */
15
+ trackGets?: boolean;
16
+ }
17
+
18
+ export interface PathStat {
19
+ path: string;
20
+ sets: number;
21
+ gets: number;
22
+ fires: number;
23
+ avgSetMs: number;
24
+ peakSetMs: number;
25
+ subscriberCount: number;
26
+ }
27
+
28
+ export interface HotListener {
29
+ path: string;
30
+ fires: number;
31
+ firesPerSec: number;
32
+ }
33
+
34
+ export interface ActiveSub {
35
+ id: string;
36
+ path: string;
37
+ ts: number;
38
+ }
39
+
40
+ export interface TimelineEvent {
41
+ type: 'set' | 'get' | 'fire' | 'subscribe' | 'unsubscribe' | 'batch' | 'setMany';
42
+ path?: string;
43
+ paths?: string[];
44
+ dur?: number;
45
+ ts: number;
46
+ subId?: string;
47
+ _seq: number;
48
+ }
49
+
50
+ export interface PerfReport {
51
+ sessionId: string;
52
+ elapsedMs: number;
53
+ totalEvents: number;
54
+ dropped: number;
55
+ summary: {
56
+ totalSets: number;
57
+ totalGets: number;
58
+ totalFires: number;
59
+ uniquePaths: number;
60
+ totalSubscribes: number;
61
+ totalUnsubscribes: number;
62
+ activeSubscribers: number;
63
+ setsPerSec: number;
64
+ };
65
+ batches: {
66
+ count: number;
67
+ totalPaths: number;
68
+ totalCoalesced: number;
69
+ };
70
+ hotPaths: PathStat[];
71
+ hotListeners: HotListener[];
72
+ activeSubs: ActiveSub[];
73
+ timeline: TimelineEvent[];
74
+ }
75
+
76
+ export interface PerfMonitor {
77
+ /** Generate a performance report snapshot */
78
+ report(): PerfReport;
79
+ /** Download the report as a JSON file */
80
+ download(filename?: string): PerfReport;
81
+ /** Reset all collected stats */
82
+ reset(): void;
83
+ /** Unwrap the store (restore original methods) */
84
+ destroy(): void;
85
+ /** Raw timeline array */
86
+ readonly timeline: TimelineEvent[];
87
+ /** Raw per-path stats map */
88
+ readonly pathStats: Map<string, any>;
89
+ /** Session identifier */
90
+ readonly sessionId: string;
91
+ /** Reference to the wrapped store (used by State Tree tab) */
92
+ readonly store: EveryStateStore;
93
+ }
94
+
95
+ /**
96
+ * Create a performance monitor that wraps an EveryState store.
97
+ * Must be called BEFORE any subscriptions for full visibility.
98
+ */
99
+ export function createPerfMonitor(store: EveryStateStore, options?: PerfMonitorOptions): PerfMonitor;
100
+
101
+ /**
102
+ * Mount a floating overlay in the browser that displays live perf stats.
103
+ * Uses Shadow DOM for style encapsulation.
104
+ * @returns Unmount function
105
+ */
106
+ export function mountOverlay(monitor: PerfMonitor, container: HTMLElement): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystate/perf",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "EveryState Performance Monitor: Non-invasive performance monitoring with method wrapping, path heatmaps, timeline recording, browser overlay",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -31,9 +31,8 @@
31
31
  "test": "node self-test.js"
32
32
  },
33
33
  "files": [
34
- "index.js",
35
- "perfMonitor.js",
36
- "overlay.js",
37
- "README.md"
34
+ "*.js",
35
+ "*.d.ts",
36
+ "*.md"
38
37
  ]
39
38
  }
package/self-test.js ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * @everystate/perf: self-test.js
3
+ *
4
+ * Zero-dependency self-test. Runs on postinstall.
5
+ * Tests the perfMonitor in Node (no DOM, no overlay).
6
+ */
7
+
8
+ import { createPerfMonitor } from './perfMonitor.js';
9
+
10
+ // Minimal EveryState mock (matches the API surface we wrap)
11
+ function createMockStore() {
12
+ const state = {};
13
+ const listeners = new Map();
14
+
15
+ const store = {
16
+ get(path) {
17
+ const parts = path.split('.');
18
+ let cur = state;
19
+ for (const p of parts) {
20
+ if (cur == null) return undefined;
21
+ cur = cur[p];
22
+ }
23
+ return cur;
24
+ },
25
+ set(path, value) {
26
+ const parts = path.split('.');
27
+ const key = parts.pop();
28
+ let cur = state;
29
+ for (const p of parts) {
30
+ if (!cur[p]) cur[p] = {};
31
+ cur = cur[p];
32
+ }
33
+ cur[key] = value;
34
+
35
+ // Notify exact listeners
36
+ const exact = listeners.get(path);
37
+ if (exact) exact.forEach(cb => cb(value, { path, value }));
38
+
39
+ // Notify wildcard *
40
+ const global = listeners.get('*');
41
+ if (global) global.forEach(cb => cb({ path, value }));
42
+
43
+ return value;
44
+ },
45
+ subscribe(path, handler) {
46
+ if (!listeners.has(path)) listeners.set(path, new Set());
47
+ listeners.get(path).add(handler);
48
+ return () => {
49
+ const set = listeners.get(path);
50
+ if (set) {
51
+ set.delete(handler);
52
+ if (set.size === 0) listeners.delete(path);
53
+ }
54
+ };
55
+ },
56
+ batch(fn) {
57
+ fn();
58
+ },
59
+ setMany(entries) {
60
+ if (entries && typeof entries === 'object' && !Array.isArray(entries) && !(entries instanceof Map)) {
61
+ for (const [p, v] of Object.entries(entries)) {
62
+ store.set(p, v);
63
+ }
64
+ }
65
+ },
66
+ };
67
+ return store;
68
+ }
69
+
70
+ let passed = 0;
71
+ let failed = 0;
72
+
73
+ function assert(label, condition) {
74
+ if (condition) {
75
+ passed++;
76
+ console.log(` ✓ ${label}`);
77
+ } else {
78
+ failed++;
79
+ console.error(` ✗ ${label}`);
80
+ }
81
+ }
82
+
83
+ // == Tests ================================================================
84
+
85
+ console.log('\n1. createPerfMonitor wraps store methods');
86
+ {
87
+ const store = createMockStore();
88
+ const origSet = store.set;
89
+ const origSubscribe = store.subscribe;
90
+ const origBatch = store.batch;
91
+ const origSetMany = store.setMany;
92
+
93
+ const perf = createPerfMonitor(store);
94
+
95
+ assert('set is wrapped', store.set !== origSet);
96
+ assert('subscribe is wrapped', store.subscribe !== origSubscribe);
97
+ assert('batch is wrapped', store.batch !== origBatch);
98
+ assert('setMany is wrapped', store.setMany !== origSetMany);
99
+
100
+ perf.destroy();
101
+ assert('set is restored after destroy', store.set === origSet);
102
+ assert('subscribe is restored after destroy', store.subscribe === origSubscribe);
103
+ }
104
+
105
+ console.log('\n2. tracks set events');
106
+ {
107
+ const store = createMockStore();
108
+ const perf = createPerfMonitor(store);
109
+
110
+ store.set('count', 1);
111
+ store.set('count', 2);
112
+ store.set('name', 'Alice');
113
+
114
+ const r = perf.report();
115
+ assert('totalSets = 3', r.summary.totalSets === 3);
116
+ assert('uniquePaths = 2', r.summary.uniquePaths === 2);
117
+ assert('hotPaths[0] is count (2 sets)', r.hotPaths[0].path === 'count' && r.hotPaths[0].sets === 2);
118
+ assert('hotPaths[1] is name (1 set)', r.hotPaths[1].path === 'name' && r.hotPaths[1].sets === 1);
119
+
120
+ perf.destroy();
121
+ }
122
+
123
+ console.log('\n3. tracks subscribe and fire events');
124
+ {
125
+ const store = createMockStore();
126
+ const perf = createPerfMonitor(store);
127
+
128
+ let fires = 0;
129
+ const unsub = store.subscribe('count', () => { fires++; });
130
+
131
+ store.set('count', 1);
132
+ store.set('count', 2);
133
+
134
+ const r = perf.report();
135
+ assert('totalSubscribes = 1', r.summary.totalSubscribes === 1);
136
+ assert('activeSubscribers = 1', r.summary.activeSubscribers === 1);
137
+ assert('totalFires = 2', r.summary.totalFires === 2);
138
+ assert('handler actually fired', fires === 2);
139
+
140
+ unsub();
141
+ const r2 = perf.report();
142
+ assert('totalUnsubscribes = 1 after unsub', r2.summary.totalUnsubscribes === 1);
143
+ assert('activeSubscribers = 0 after unsub', r2.summary.activeSubscribers === 0);
144
+
145
+ perf.destroy();
146
+ }
147
+
148
+ console.log('\n4. tracks batch events');
149
+ {
150
+ const store = createMockStore();
151
+ const perf = createPerfMonitor(store);
152
+
153
+ store.batch(() => {
154
+ store.set('a', 1);
155
+ store.set('b', 2);
156
+ store.set('a', 3); // duplicate path
157
+ });
158
+
159
+ const r = perf.report();
160
+ assert('batch count = 1', r.batches.count === 1);
161
+ assert('batch totalPaths = 3', r.batches.totalPaths === 3);
162
+ assert('batch coalesced = 1 (duplicate a)', r.batches.totalCoalesced === 1);
163
+
164
+ perf.destroy();
165
+ }
166
+
167
+ console.log('\n5. tracks setMany events');
168
+ {
169
+ const store = createMockStore();
170
+ const perf = createPerfMonitor(store);
171
+
172
+ store.setMany({ x: 1, y: 2, z: 3 });
173
+
174
+ const r = perf.report();
175
+ const setManyEvents = r.timeline.filter(e => e.type === 'setMany');
176
+ assert('setMany event recorded', setManyEvents.length === 1);
177
+ assert('setMany pathCount = 3', setManyEvents[0].pathCount === 3);
178
+
179
+ perf.destroy();
180
+ }
181
+
182
+ console.log('\n6. report structure');
183
+ {
184
+ const store = createMockStore();
185
+ const perf = createPerfMonitor(store);
186
+
187
+ store.set('x', 1);
188
+
189
+ const r = perf.report();
190
+ assert('has sessionId', typeof r.sessionId === 'string' && r.sessionId.length > 0);
191
+ assert('has elapsedMs', typeof r.elapsedMs === 'number');
192
+ assert('has summary', typeof r.summary === 'object');
193
+ assert('has batches', typeof r.batches === 'object');
194
+ assert('has hotPaths', Array.isArray(r.hotPaths));
195
+ assert('has hotListeners', Array.isArray(r.hotListeners));
196
+ assert('has activeSubs', Array.isArray(r.activeSubs));
197
+ assert('has timeline', Array.isArray(r.timeline));
198
+
199
+ perf.destroy();
200
+ }
201
+
202
+ console.log('\n7. reset clears all data');
203
+ {
204
+ const store = createMockStore();
205
+ const perf = createPerfMonitor(store);
206
+
207
+ store.set('x', 1);
208
+ store.set('x', 2);
209
+
210
+ perf.reset();
211
+ const r = perf.report();
212
+ assert('totalEvents = 0 after reset', r.totalEvents === 0);
213
+ assert('totalSets = 0 after reset', r.summary.totalSets === 0);
214
+ assert('uniquePaths = 0 after reset', r.summary.uniquePaths === 0);
215
+ assert('timeline empty after reset', r.timeline.length === 0);
216
+
217
+ perf.destroy();
218
+ }
219
+
220
+ console.log('\n8. ring buffer drops old events');
221
+ {
222
+ const store = createMockStore();
223
+ const perf = createPerfMonitor(store, { maxEvents: 5 });
224
+
225
+ for (let i = 0; i < 10; i++) {
226
+ store.set('x', i);
227
+ }
228
+
229
+ const r = perf.report();
230
+ assert('totalEvents = 10', r.totalEvents === 10);
231
+ assert('timeline length capped at 5', perf.timeline.length === 5);
232
+ assert('dropped = 5', r.dropped === 5);
233
+ assert('oldest event is seq 5', perf.timeline[0]._seq === 5);
234
+
235
+ perf.destroy();
236
+ }
237
+
238
+ console.log('\n9. trackValues option');
239
+ {
240
+ const store = createMockStore();
241
+ const perfNoVals = createPerfMonitor(store);
242
+ store.set('x', 42);
243
+ const evtNoVal = perfNoVals.timeline.find(e => e.type === 'set');
244
+ assert('value not tracked by default', evtNoVal.value === undefined);
245
+ perfNoVals.destroy();
246
+
247
+ const store2 = createMockStore();
248
+ const perfWithVals = createPerfMonitor(store2, { trackValues: true });
249
+ store2.set('x', 42);
250
+ const evtWithVal = perfWithVals.timeline.find(e => e.type === 'set');
251
+ assert('value tracked when enabled', evtWithVal.value === 42);
252
+ perfWithVals.destroy();
253
+ }
254
+
255
+ console.log('\n10. trackGets option');
256
+ {
257
+ const store = createMockStore();
258
+ const perf = createPerfMonitor(store, { trackGets: true });
259
+ store.set('x', 1);
260
+ store.get('x');
261
+ store.get('x');
262
+
263
+ const r = perf.report();
264
+ const xStats = r.hotPaths.find(p => p.path === 'x');
265
+ assert('gets tracked when enabled', xStats.gets === 2);
266
+
267
+ perf.destroy();
268
+ }
269
+
270
+ console.log('\n11. per-path timing');
271
+ {
272
+ const store = createMockStore();
273
+ const perf = createPerfMonitor(store);
274
+
275
+ store.set('a', 1);
276
+ store.set('a', 2);
277
+ store.set('a', 3);
278
+
279
+ const r = perf.report();
280
+ const aStats = r.hotPaths.find(p => p.path === 'a');
281
+ assert('avgSetMs is a number', typeof aStats.avgSetMs === 'number');
282
+ assert('peakSetMs >= avgSetMs', aStats.peakSetMs >= aStats.avgSetMs);
283
+
284
+ perf.destroy();
285
+ }
286
+
287
+ console.log('\n12. download returns report object');
288
+ {
289
+ const store = createMockStore();
290
+ const perf = createPerfMonitor(store);
291
+ store.set('x', 1);
292
+
293
+ // In Node there's no document, so download just returns data
294
+ const data = perf.download('test.json');
295
+ assert('download returns data', data !== undefined);
296
+ assert('download data has sessionId', typeof data.sessionId === 'string');
297
+ assert('download data has full timeline', Array.isArray(data.timeline));
298
+
299
+ perf.destroy();
300
+ }
301
+
302
+ // == Summary ==============================================================
303
+
304
+ console.log(`\n@everystate/perf v1.0.0 self-test`);
305
+ console.log(`✓ ${passed} assertions passed${failed ? `, ✗ ${failed} failed` : ''}\n`);
306
+
307
+ if (failed > 0) process.exit(1);