@servlyadmin/runtime-core 0.1.2 → 0.1.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/dist/index.cjs +825 -63
- package/dist/index.d.cts +432 -3
- package/dist/index.d.ts +432 -3
- package/dist/index.js +811 -63
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,6 +7,516 @@ import {
|
|
|
7
7
|
extractDependenciesFromCode
|
|
8
8
|
} from "./chunk-CIUQK4GA.js";
|
|
9
9
|
|
|
10
|
+
// src/analyticsTypes.ts
|
|
11
|
+
var DEFAULT_ANALYTICS_CONFIG = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
endpoint: "/api/v1/analytics/events",
|
|
14
|
+
batchSize: 50,
|
|
15
|
+
flushInterval: 3e4,
|
|
16
|
+
// 30 seconds
|
|
17
|
+
sampleRate: 1,
|
|
18
|
+
environment: "production",
|
|
19
|
+
debug: false
|
|
20
|
+
};
|
|
21
|
+
var MAX_ERROR_MESSAGE_LENGTH = 1e3;
|
|
22
|
+
var MAX_STACK_TRACE_LENGTH = 500;
|
|
23
|
+
var MAX_QUEUE_SIZE = 500;
|
|
24
|
+
var MAX_RETRY_ATTEMPTS = 3;
|
|
25
|
+
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
26
|
+
var SDK_VERSION = "1.0.0";
|
|
27
|
+
|
|
28
|
+
// src/sessionManager.ts
|
|
29
|
+
var SESSION_STORAGE_KEY = "servly_analytics_session";
|
|
30
|
+
function generateUUID() {
|
|
31
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
32
|
+
return crypto.randomUUID();
|
|
33
|
+
}
|
|
34
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
35
|
+
const r = Math.random() * 16 | 0;
|
|
36
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
37
|
+
return v.toString(16);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function isSessionStorageAvailable() {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof sessionStorage === "undefined") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const testKey = "__servly_test__";
|
|
46
|
+
sessionStorage.setItem(testKey, "test");
|
|
47
|
+
sessionStorage.removeItem(testKey);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function loadSession() {
|
|
54
|
+
if (!isSessionStorageAvailable()) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const stored = sessionStorage.getItem(SESSION_STORAGE_KEY);
|
|
59
|
+
if (!stored) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return JSON.parse(stored);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function saveSession(session) {
|
|
68
|
+
if (!isSessionStorageAvailable()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function clearSession() {
|
|
77
|
+
if (!isSessionStorageAvailable()) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isSessionExpired(session) {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
return now - session.lastActivityAt > SESSION_TIMEOUT_MS;
|
|
88
|
+
}
|
|
89
|
+
var SessionManager = class {
|
|
90
|
+
constructor() {
|
|
91
|
+
this.session = null;
|
|
92
|
+
this.initialize();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Initialize session manager
|
|
96
|
+
*/
|
|
97
|
+
initialize() {
|
|
98
|
+
const stored = loadSession();
|
|
99
|
+
if (stored && !isSessionExpired(stored)) {
|
|
100
|
+
this.session = stored;
|
|
101
|
+
this.touch();
|
|
102
|
+
} else {
|
|
103
|
+
this.createNewSession();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Create a new session
|
|
108
|
+
*/
|
|
109
|
+
createNewSession() {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
this.session = {
|
|
112
|
+
id: generateUUID(),
|
|
113
|
+
createdAt: now,
|
|
114
|
+
lastActivityAt: now
|
|
115
|
+
};
|
|
116
|
+
saveSession(this.session);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get current session ID
|
|
120
|
+
* Creates new session if expired
|
|
121
|
+
*/
|
|
122
|
+
getSessionId() {
|
|
123
|
+
if (!this.session || isSessionExpired(this.session)) {
|
|
124
|
+
this.createNewSession();
|
|
125
|
+
}
|
|
126
|
+
return this.session.id;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Update last activity timestamp
|
|
130
|
+
*/
|
|
131
|
+
touch() {
|
|
132
|
+
if (this.session) {
|
|
133
|
+
this.session.lastActivityAt = Date.now();
|
|
134
|
+
saveSession(this.session);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Force create a new session
|
|
139
|
+
*/
|
|
140
|
+
rotate() {
|
|
141
|
+
this.createNewSession();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Clear current session
|
|
145
|
+
*/
|
|
146
|
+
clear() {
|
|
147
|
+
this.session = null;
|
|
148
|
+
clearSession();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get session info (for debugging)
|
|
152
|
+
*/
|
|
153
|
+
getSessionInfo() {
|
|
154
|
+
return this.session ? { ...this.session } : null;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var sessionManagerInstance = null;
|
|
158
|
+
function getSessionManager() {
|
|
159
|
+
if (!sessionManagerInstance) {
|
|
160
|
+
sessionManagerInstance = new SessionManager();
|
|
161
|
+
}
|
|
162
|
+
return sessionManagerInstance;
|
|
163
|
+
}
|
|
164
|
+
function resetSessionManager() {
|
|
165
|
+
if (sessionManagerInstance) {
|
|
166
|
+
sessionManagerInstance.clear();
|
|
167
|
+
}
|
|
168
|
+
sessionManagerInstance = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/analytics.ts
|
|
172
|
+
var AnalyticsCollector = class {
|
|
173
|
+
constructor(config) {
|
|
174
|
+
this.eventQueue = [];
|
|
175
|
+
this.flushTimer = null;
|
|
176
|
+
this.isFlushing = false;
|
|
177
|
+
this.retryDelay = 1e3;
|
|
178
|
+
this.config = { ...DEFAULT_ANALYTICS_CONFIG, ...config };
|
|
179
|
+
this.isEnabled = this.config.enabled;
|
|
180
|
+
this.startFlushTimer();
|
|
181
|
+
}
|
|
182
|
+
// ============================================
|
|
183
|
+
// Configuration
|
|
184
|
+
// ============================================
|
|
185
|
+
/**
|
|
186
|
+
* Configure analytics
|
|
187
|
+
*/
|
|
188
|
+
configure(config) {
|
|
189
|
+
this.config = { ...this.config, ...config };
|
|
190
|
+
this.isEnabled = this.config.enabled;
|
|
191
|
+
this.stopFlushTimer();
|
|
192
|
+
if (this.isEnabled) {
|
|
193
|
+
this.startFlushTimer();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Disable analytics collection
|
|
198
|
+
*/
|
|
199
|
+
disable() {
|
|
200
|
+
this.isEnabled = false;
|
|
201
|
+
this.stopFlushTimer();
|
|
202
|
+
this.eventQueue = [];
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Enable analytics collection
|
|
206
|
+
*/
|
|
207
|
+
enable() {
|
|
208
|
+
this.isEnabled = true;
|
|
209
|
+
this.startFlushTimer();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Destroy and cleanup
|
|
213
|
+
*/
|
|
214
|
+
destroy() {
|
|
215
|
+
this.disable();
|
|
216
|
+
}
|
|
217
|
+
// ============================================
|
|
218
|
+
// Event Tracking
|
|
219
|
+
// ============================================
|
|
220
|
+
/**
|
|
221
|
+
* Track component render
|
|
222
|
+
*/
|
|
223
|
+
trackRender(componentId, version, duration, metadata) {
|
|
224
|
+
if (!this.shouldTrack()) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const event = {
|
|
228
|
+
type: "render",
|
|
229
|
+
componentId,
|
|
230
|
+
version,
|
|
231
|
+
timestamp: Date.now(),
|
|
232
|
+
sessionId: getSessionManager().getSessionId(),
|
|
233
|
+
appId: this.config.appId,
|
|
234
|
+
duration: Math.round(duration),
|
|
235
|
+
metadata
|
|
236
|
+
};
|
|
237
|
+
this.queueEvent(event);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Track component fetch
|
|
241
|
+
*/
|
|
242
|
+
trackFetch(componentId, version, duration, fromCache, metadata) {
|
|
243
|
+
if (!this.shouldTrack()) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const event = {
|
|
247
|
+
type: "fetch",
|
|
248
|
+
componentId,
|
|
249
|
+
version,
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
sessionId: getSessionManager().getSessionId(),
|
|
252
|
+
appId: this.config.appId,
|
|
253
|
+
duration: Math.round(duration),
|
|
254
|
+
metadata: {
|
|
255
|
+
cacheHit: fromCache,
|
|
256
|
+
fetchDuration: Math.round(duration),
|
|
257
|
+
...metadata
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
this.queueEvent(event);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Track error
|
|
264
|
+
*/
|
|
265
|
+
trackError(componentId, version, error, context) {
|
|
266
|
+
if (!this.shouldTrack()) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const errorMessage = this.truncateString(
|
|
270
|
+
error.message || "Unknown error",
|
|
271
|
+
MAX_ERROR_MESSAGE_LENGTH
|
|
272
|
+
);
|
|
273
|
+
const stackTrace = error.stack ? this.truncateString(error.stack, MAX_STACK_TRACE_LENGTH) : void 0;
|
|
274
|
+
const event = {
|
|
275
|
+
type: "error",
|
|
276
|
+
componentId,
|
|
277
|
+
version,
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
sessionId: getSessionManager().getSessionId(),
|
|
280
|
+
appId: this.config.appId,
|
|
281
|
+
metadata: {
|
|
282
|
+
errorType: context?.errorType || "render",
|
|
283
|
+
errorMessage,
|
|
284
|
+
stackTrace,
|
|
285
|
+
bindingPath: context?.bindingPath,
|
|
286
|
+
elementId: context?.elementId
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
this.queueEvent(event);
|
|
290
|
+
}
|
|
291
|
+
// ============================================
|
|
292
|
+
// Queue Management
|
|
293
|
+
// ============================================
|
|
294
|
+
/**
|
|
295
|
+
* Check if event should be tracked (sampling + enabled)
|
|
296
|
+
*/
|
|
297
|
+
shouldTrack() {
|
|
298
|
+
if (!this.isEnabled) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
if (this.config.sampleRate < 1) {
|
|
302
|
+
return Math.random() < this.config.sampleRate;
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Add event to queue
|
|
308
|
+
*/
|
|
309
|
+
queueEvent(event) {
|
|
310
|
+
if (this.eventQueue.length >= MAX_QUEUE_SIZE) {
|
|
311
|
+
this.eventQueue.shift();
|
|
312
|
+
this.log("Queue full, dropping oldest event");
|
|
313
|
+
}
|
|
314
|
+
this.eventQueue.push({
|
|
315
|
+
event,
|
|
316
|
+
retryCount: 0,
|
|
317
|
+
addedAt: Date.now()
|
|
318
|
+
});
|
|
319
|
+
if (this.eventQueue.length >= this.config.batchSize) {
|
|
320
|
+
this.flush();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Get current queue size
|
|
325
|
+
*/
|
|
326
|
+
getQueueSize() {
|
|
327
|
+
return this.eventQueue.length;
|
|
328
|
+
}
|
|
329
|
+
// ============================================
|
|
330
|
+
// Flush Logic
|
|
331
|
+
// ============================================
|
|
332
|
+
/**
|
|
333
|
+
* Start the flush timer
|
|
334
|
+
*/
|
|
335
|
+
startFlushTimer() {
|
|
336
|
+
if (this.flushTimer) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this.flushTimer = setInterval(() => {
|
|
340
|
+
this.flush();
|
|
341
|
+
}, this.config.flushInterval);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Stop the flush timer
|
|
345
|
+
*/
|
|
346
|
+
stopFlushTimer() {
|
|
347
|
+
if (this.flushTimer) {
|
|
348
|
+
clearInterval(this.flushTimer);
|
|
349
|
+
this.flushTimer = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Flush events to server
|
|
354
|
+
*/
|
|
355
|
+
async flush() {
|
|
356
|
+
if (this.isFlushing || this.eventQueue.length === 0) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.isFlushing = true;
|
|
360
|
+
if (typeof requestIdleCallback !== "undefined") {
|
|
361
|
+
requestIdleCallback(
|
|
362
|
+
() => {
|
|
363
|
+
this.doFlush();
|
|
364
|
+
},
|
|
365
|
+
{ timeout: 5e3 }
|
|
366
|
+
);
|
|
367
|
+
} else {
|
|
368
|
+
setTimeout(() => {
|
|
369
|
+
this.doFlush();
|
|
370
|
+
}, 0);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Perform the actual flush
|
|
375
|
+
*/
|
|
376
|
+
async doFlush() {
|
|
377
|
+
const eventsToSend = this.eventQueue.splice(0, this.config.batchSize);
|
|
378
|
+
if (eventsToSend.length === 0) {
|
|
379
|
+
this.isFlushing = false;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const request = {
|
|
383
|
+
events: eventsToSend.map((q) => q.event),
|
|
384
|
+
clientInfo: {
|
|
385
|
+
sdkVersion: SDK_VERSION,
|
|
386
|
+
environment: this.config.environment
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
try {
|
|
390
|
+
const response = await this.sendEvents(request);
|
|
391
|
+
if (response.success) {
|
|
392
|
+
this.log(`Flushed ${response.accepted} events`);
|
|
393
|
+
this.retryDelay = 1e3;
|
|
394
|
+
} else {
|
|
395
|
+
this.handleFailedEvents(eventsToSend, response);
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
this.handleNetworkError(eventsToSend, error);
|
|
399
|
+
} finally {
|
|
400
|
+
this.isFlushing = false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Send events to the API
|
|
405
|
+
*/
|
|
406
|
+
async sendEvents(request) {
|
|
407
|
+
const headers = {
|
|
408
|
+
"Content-Type": "application/json"
|
|
409
|
+
};
|
|
410
|
+
if (this.config.apiKey) {
|
|
411
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
412
|
+
}
|
|
413
|
+
const response = await fetch(this.config.endpoint, {
|
|
414
|
+
method: "POST",
|
|
415
|
+
headers,
|
|
416
|
+
body: JSON.stringify(request)
|
|
417
|
+
});
|
|
418
|
+
if (response.status === 429) {
|
|
419
|
+
const retryAfter = parseInt(response.headers.get("Retry-After") || "60", 10);
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
accepted: 0,
|
|
423
|
+
rejected: request.events.length,
|
|
424
|
+
retryAfter
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
429
|
+
}
|
|
430
|
+
return await response.json();
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Handle failed events (partial success)
|
|
434
|
+
*/
|
|
435
|
+
handleFailedEvents(events, response) {
|
|
436
|
+
const eventsToRetry = events.filter((e) => e.retryCount < MAX_RETRY_ATTEMPTS);
|
|
437
|
+
eventsToRetry.forEach((e) => {
|
|
438
|
+
e.retryCount++;
|
|
439
|
+
this.eventQueue.unshift(e);
|
|
440
|
+
});
|
|
441
|
+
const dropped = events.length - eventsToRetry.length;
|
|
442
|
+
if (dropped > 0) {
|
|
443
|
+
this.log(`Dropped ${dropped} events after max retries`);
|
|
444
|
+
}
|
|
445
|
+
if (response.retryAfter) {
|
|
446
|
+
this.scheduleRetry(response.retryAfter * 1e3);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Handle network error
|
|
451
|
+
*/
|
|
452
|
+
handleNetworkError(events, error) {
|
|
453
|
+
this.log(`Network error: ${error}`);
|
|
454
|
+
const eventsToRetry = events.filter((e) => e.retryCount < MAX_RETRY_ATTEMPTS);
|
|
455
|
+
eventsToRetry.forEach((e) => {
|
|
456
|
+
e.retryCount++;
|
|
457
|
+
this.eventQueue.unshift(e);
|
|
458
|
+
});
|
|
459
|
+
this.scheduleRetry(this.retryDelay);
|
|
460
|
+
this.retryDelay = Math.min(this.retryDelay * 2, 3e4);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Schedule a retry flush
|
|
464
|
+
*/
|
|
465
|
+
scheduleRetry(delayMs) {
|
|
466
|
+
setTimeout(() => {
|
|
467
|
+
this.flush();
|
|
468
|
+
}, delayMs);
|
|
469
|
+
}
|
|
470
|
+
// ============================================
|
|
471
|
+
// Utilities
|
|
472
|
+
// ============================================
|
|
473
|
+
/**
|
|
474
|
+
* Truncate string to max length
|
|
475
|
+
*/
|
|
476
|
+
truncateString(str, maxLength) {
|
|
477
|
+
if (str.length <= maxLength) {
|
|
478
|
+
return str;
|
|
479
|
+
}
|
|
480
|
+
return str.substring(0, maxLength - 3) + "...";
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Log debug message
|
|
484
|
+
*/
|
|
485
|
+
log(message) {
|
|
486
|
+
if (this.config.debug) {
|
|
487
|
+
console.log(`[Analytics] ${message}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
var analyticsInstance = null;
|
|
492
|
+
function getAnalytics() {
|
|
493
|
+
if (!analyticsInstance) {
|
|
494
|
+
analyticsInstance = new AnalyticsCollector();
|
|
495
|
+
}
|
|
496
|
+
return analyticsInstance;
|
|
497
|
+
}
|
|
498
|
+
function configureAnalytics(config) {
|
|
499
|
+
getAnalytics().configure(config);
|
|
500
|
+
}
|
|
501
|
+
function resetAnalytics() {
|
|
502
|
+
if (analyticsInstance) {
|
|
503
|
+
analyticsInstance.destroy();
|
|
504
|
+
}
|
|
505
|
+
analyticsInstance = null;
|
|
506
|
+
}
|
|
507
|
+
var analytics = {
|
|
508
|
+
get instance() {
|
|
509
|
+
return getAnalytics();
|
|
510
|
+
},
|
|
511
|
+
configure: configureAnalytics,
|
|
512
|
+
trackRender: (componentId, version, duration, metadata) => getAnalytics().trackRender(componentId, version, duration, metadata),
|
|
513
|
+
trackFetch: (componentId, version, duration, fromCache, metadata) => getAnalytics().trackFetch(componentId, version, duration, fromCache, metadata),
|
|
514
|
+
trackError: (componentId, version, error, context) => getAnalytics().trackError(componentId, version, error, context),
|
|
515
|
+
flush: () => getAnalytics().flush(),
|
|
516
|
+
disable: () => getAnalytics().disable(),
|
|
517
|
+
enable: () => getAnalytics().enable()
|
|
518
|
+
};
|
|
519
|
+
|
|
10
520
|
// src/bindings.ts
|
|
11
521
|
var BINDING_SOURCES = [
|
|
12
522
|
"props",
|
|
@@ -113,44 +623,64 @@ function resolveTernaryValue(value, context) {
|
|
|
113
623
|
if (value === "undefined") return void 0;
|
|
114
624
|
return resolveExpression(value, context);
|
|
115
625
|
}
|
|
116
|
-
function resolveTemplate(template, context) {
|
|
626
|
+
function resolveTemplate(template, context, componentId) {
|
|
117
627
|
if (!template || typeof template !== "string") {
|
|
118
628
|
return template;
|
|
119
629
|
}
|
|
120
630
|
if (!hasTemplateSyntax(template)) {
|
|
121
631
|
return template;
|
|
122
632
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return template.replace(TEMPLATE_REGEX, (match, expression) => {
|
|
132
|
-
const value = resolveExpression(expression, context);
|
|
133
|
-
if (value === void 0 || value === null) {
|
|
134
|
-
return "";
|
|
633
|
+
try {
|
|
634
|
+
const singleMatch = template.match(/^\{\{([^}]+)\}\}$/);
|
|
635
|
+
if (singleMatch) {
|
|
636
|
+
const value = resolveExpression(singleMatch[1], context);
|
|
637
|
+
if (value === void 0 || value === null) {
|
|
638
|
+
return "";
|
|
639
|
+
}
|
|
640
|
+
return String(value);
|
|
135
641
|
}
|
|
136
|
-
|
|
137
|
-
|
|
642
|
+
return template.replace(TEMPLATE_REGEX, (match, expression) => {
|
|
643
|
+
const value = resolveExpression(expression, context);
|
|
644
|
+
if (value === void 0 || value === null) {
|
|
645
|
+
return "";
|
|
646
|
+
}
|
|
647
|
+
if (typeof value === "object") {
|
|
648
|
+
return JSON.stringify(value);
|
|
649
|
+
}
|
|
650
|
+
return String(value);
|
|
651
|
+
});
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (componentId) {
|
|
654
|
+
analytics.trackError(componentId, "latest", error, {
|
|
655
|
+
errorType: "binding",
|
|
656
|
+
bindingPath: template
|
|
657
|
+
});
|
|
138
658
|
}
|
|
139
|
-
return
|
|
140
|
-
}
|
|
659
|
+
return "";
|
|
660
|
+
}
|
|
141
661
|
}
|
|
142
|
-
function resolveTemplateValue(template, context) {
|
|
662
|
+
function resolveTemplateValue(template, context, componentId) {
|
|
143
663
|
if (!template || typeof template !== "string") {
|
|
144
664
|
return template;
|
|
145
665
|
}
|
|
146
666
|
if (!hasTemplateSyntax(template)) {
|
|
147
667
|
return template;
|
|
148
668
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
669
|
+
try {
|
|
670
|
+
const singleMatch = template.match(/^\{\{([^}]+)\}\}$/);
|
|
671
|
+
if (singleMatch) {
|
|
672
|
+
return resolveExpression(singleMatch[1], context);
|
|
673
|
+
}
|
|
674
|
+
return resolveTemplate(template, context, componentId);
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (componentId) {
|
|
677
|
+
analytics.trackError(componentId, "latest", error, {
|
|
678
|
+
errorType: "binding",
|
|
679
|
+
bindingPath: template
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
return void 0;
|
|
152
683
|
}
|
|
153
|
-
return resolveTemplate(template, context);
|
|
154
684
|
}
|
|
155
685
|
function isPlainObject(value) {
|
|
156
686
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
@@ -383,6 +913,162 @@ function updateStyles(element, oldStyles, newStyles) {
|
|
|
383
913
|
}
|
|
384
914
|
}
|
|
385
915
|
|
|
916
|
+
// src/memorySampler.ts
|
|
917
|
+
var MemorySampler = class {
|
|
918
|
+
constructor() {
|
|
919
|
+
this.isSupported = this.checkSupport();
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Check if memory API is available
|
|
923
|
+
*/
|
|
924
|
+
checkSupport() {
|
|
925
|
+
if (typeof performance === "undefined") {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
const perf = performance;
|
|
929
|
+
return typeof perf.memory !== "undefined";
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Check if memory sampling is available
|
|
933
|
+
*/
|
|
934
|
+
isAvailable() {
|
|
935
|
+
return this.isSupported;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Take a memory sample
|
|
939
|
+
* Returns null if memory API is not available
|
|
940
|
+
*/
|
|
941
|
+
sample() {
|
|
942
|
+
if (!this.isSupported) {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
const perf = performance;
|
|
946
|
+
if (!perf.memory) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
heapUsedKB: Math.round(perf.memory.usedJSHeapSize / 1024),
|
|
951
|
+
heapTotalKB: Math.round(perf.memory.totalJSHeapSize / 1024),
|
|
952
|
+
timestamp: Date.now()
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Calculate heap delta between two samples
|
|
957
|
+
* Returns the difference in KB (positive = memory increased)
|
|
958
|
+
*/
|
|
959
|
+
calculateDelta(before, after) {
|
|
960
|
+
if (!before || !after) {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
return after.heapUsedKB - before.heapUsedKB;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
var memorySamplerInstance = null;
|
|
967
|
+
function getMemorySampler() {
|
|
968
|
+
if (!memorySamplerInstance) {
|
|
969
|
+
memorySamplerInstance = new MemorySampler();
|
|
970
|
+
}
|
|
971
|
+
return memorySamplerInstance;
|
|
972
|
+
}
|
|
973
|
+
function resetMemorySampler() {
|
|
974
|
+
memorySamplerInstance = null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/longTaskObserver.ts
|
|
978
|
+
var LongTaskObserver = class {
|
|
979
|
+
constructor() {
|
|
980
|
+
this.observer = null;
|
|
981
|
+
this.longTaskCount = 0;
|
|
982
|
+
this.isObserving = false;
|
|
983
|
+
this.isSupported = this.checkSupport();
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Check if PerformanceObserver with longtask support is available
|
|
987
|
+
*/
|
|
988
|
+
checkSupport() {
|
|
989
|
+
if (typeof PerformanceObserver === "undefined") {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
const supportedTypes = PerformanceObserver.supportedEntryTypes;
|
|
994
|
+
return Array.isArray(supportedTypes) && supportedTypes.includes("longtask");
|
|
995
|
+
} catch {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Check if long task observation is available
|
|
1001
|
+
*/
|
|
1002
|
+
isAvailable() {
|
|
1003
|
+
return this.isSupported;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Start observing long tasks
|
|
1007
|
+
*/
|
|
1008
|
+
start() {
|
|
1009
|
+
if (!this.isSupported || this.isObserving) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
this.longTaskCount = 0;
|
|
1013
|
+
try {
|
|
1014
|
+
this.observer = new PerformanceObserver((list) => {
|
|
1015
|
+
const entries = list.getEntries();
|
|
1016
|
+
this.longTaskCount += entries.length;
|
|
1017
|
+
});
|
|
1018
|
+
this.observer.observe({ entryTypes: ["longtask"] });
|
|
1019
|
+
this.isObserving = true;
|
|
1020
|
+
} catch {
|
|
1021
|
+
this.isSupported = false;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Stop observing and return the count of long tasks
|
|
1026
|
+
*/
|
|
1027
|
+
stop() {
|
|
1028
|
+
if (!this.isObserving || !this.observer) {
|
|
1029
|
+
return this.longTaskCount;
|
|
1030
|
+
}
|
|
1031
|
+
try {
|
|
1032
|
+
this.observer.disconnect();
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
this.observer = null;
|
|
1036
|
+
this.isObserving = false;
|
|
1037
|
+
return this.longTaskCount;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Reset the long task counter
|
|
1041
|
+
*/
|
|
1042
|
+
reset() {
|
|
1043
|
+
this.longTaskCount = 0;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Get current count without stopping observation
|
|
1047
|
+
*/
|
|
1048
|
+
getCount() {
|
|
1049
|
+
return this.longTaskCount;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Check if currently observing
|
|
1053
|
+
*/
|
|
1054
|
+
isActive() {
|
|
1055
|
+
return this.isObserving;
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
var longTaskObserverInstance = null;
|
|
1059
|
+
function getLongTaskObserver() {
|
|
1060
|
+
if (!longTaskObserverInstance) {
|
|
1061
|
+
longTaskObserverInstance = new LongTaskObserver();
|
|
1062
|
+
}
|
|
1063
|
+
return longTaskObserverInstance;
|
|
1064
|
+
}
|
|
1065
|
+
function resetLongTaskObserver() {
|
|
1066
|
+
if (longTaskObserverInstance) {
|
|
1067
|
+
longTaskObserverInstance.stop();
|
|
1068
|
+
}
|
|
1069
|
+
longTaskObserverInstance = null;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
386
1072
|
// src/renderer.ts
|
|
387
1073
|
var COMPONENT_TO_TAG = {
|
|
388
1074
|
container: "div",
|
|
@@ -636,52 +1322,84 @@ function renderElement(element, tree, context, eventHandlers, elementStates, sta
|
|
|
636
1322
|
}
|
|
637
1323
|
function render(options) {
|
|
638
1324
|
const { container, elements, context, eventHandlers, componentRegistry, onDependencyNeeded } = options;
|
|
639
|
-
const
|
|
1325
|
+
const startTime = performance.now();
|
|
1326
|
+
const memorySampler = getMemorySampler();
|
|
1327
|
+
const longTaskObserver = getLongTaskObserver();
|
|
1328
|
+
const memoryBefore = memorySampler.sample();
|
|
1329
|
+
longTaskObserver.start();
|
|
640
1330
|
const rootElements = elements.filter((el) => !el.parent || el.parent === null);
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
};
|
|
652
|
-
container.innerHTML = "";
|
|
653
|
-
if (rootElements.length === 0) {
|
|
654
|
-
return {
|
|
1331
|
+
const componentId = rootElements[0]?.componentId || "unknown";
|
|
1332
|
+
const version = "latest";
|
|
1333
|
+
try {
|
|
1334
|
+
const tree = buildTree(elements);
|
|
1335
|
+
const state = {
|
|
1336
|
+
container,
|
|
1337
|
+
elements,
|
|
1338
|
+
context,
|
|
1339
|
+
eventHandlers,
|
|
1340
|
+
elementStates: /* @__PURE__ */ new Map(),
|
|
655
1341
|
rootElement: null,
|
|
1342
|
+
componentRegistry,
|
|
1343
|
+
onDependencyNeeded,
|
|
1344
|
+
renderingStack: /* @__PURE__ */ new Set()
|
|
1345
|
+
};
|
|
1346
|
+
container.innerHTML = "";
|
|
1347
|
+
if (rootElements.length === 0) {
|
|
1348
|
+
const duration2 = performance.now() - startTime;
|
|
1349
|
+
const longTasks2 = longTaskObserver.stop();
|
|
1350
|
+
analytics.trackRender(componentId, version, duration2, {
|
|
1351
|
+
elementCount: 0,
|
|
1352
|
+
longTaskCount: longTasks2
|
|
1353
|
+
});
|
|
1354
|
+
return {
|
|
1355
|
+
rootElement: null,
|
|
1356
|
+
update: (newContext) => update(state, newContext),
|
|
1357
|
+
destroy: () => destroy(state)
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
if (rootElements.length === 1) {
|
|
1361
|
+
state.rootElement = renderElement(
|
|
1362
|
+
rootElements[0],
|
|
1363
|
+
tree,
|
|
1364
|
+
context,
|
|
1365
|
+
eventHandlers,
|
|
1366
|
+
state.elementStates,
|
|
1367
|
+
state
|
|
1368
|
+
);
|
|
1369
|
+
container.appendChild(state.rootElement);
|
|
1370
|
+
} else {
|
|
1371
|
+
const wrapper = document.createElement("div");
|
|
1372
|
+
wrapper.setAttribute("data-servly-wrapper", "true");
|
|
1373
|
+
for (const root of rootElements) {
|
|
1374
|
+
const rootElement = renderElement(root, tree, context, eventHandlers, state.elementStates, state);
|
|
1375
|
+
wrapper.appendChild(rootElement);
|
|
1376
|
+
}
|
|
1377
|
+
state.rootElement = wrapper;
|
|
1378
|
+
container.appendChild(wrapper);
|
|
1379
|
+
}
|
|
1380
|
+
const duration = performance.now() - startTime;
|
|
1381
|
+
const memoryAfter = memorySampler.sample();
|
|
1382
|
+
const longTasks = longTaskObserver.stop();
|
|
1383
|
+
const heapDelta = memorySampler.calculateDelta(memoryBefore, memoryAfter);
|
|
1384
|
+
analytics.trackRender(componentId, version, duration, {
|
|
1385
|
+
elementCount: elements.length,
|
|
1386
|
+
heapDeltaKB: heapDelta ?? void 0,
|
|
1387
|
+
longTaskCount: longTasks,
|
|
1388
|
+
hasEventHandlers: !!eventHandlers && Object.keys(eventHandlers).length > 0,
|
|
1389
|
+
hasDependencies: !!componentRegistry
|
|
1390
|
+
});
|
|
1391
|
+
return {
|
|
1392
|
+
rootElement: state.rootElement,
|
|
656
1393
|
update: (newContext) => update(state, newContext),
|
|
657
1394
|
destroy: () => destroy(state)
|
|
658
1395
|
};
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
longTaskObserver.stop();
|
|
1398
|
+
analytics.trackError(componentId, version, error, {
|
|
1399
|
+
errorType: "render"
|
|
1400
|
+
});
|
|
1401
|
+
throw error;
|
|
659
1402
|
}
|
|
660
|
-
if (rootElements.length === 1) {
|
|
661
|
-
state.rootElement = renderElement(
|
|
662
|
-
rootElements[0],
|
|
663
|
-
tree,
|
|
664
|
-
context,
|
|
665
|
-
eventHandlers,
|
|
666
|
-
state.elementStates,
|
|
667
|
-
state
|
|
668
|
-
);
|
|
669
|
-
container.appendChild(state.rootElement);
|
|
670
|
-
} else {
|
|
671
|
-
const wrapper = document.createElement("div");
|
|
672
|
-
wrapper.setAttribute("data-servly-wrapper", "true");
|
|
673
|
-
for (const root of rootElements) {
|
|
674
|
-
const rootElement = renderElement(root, tree, context, eventHandlers, state.elementStates, state);
|
|
675
|
-
wrapper.appendChild(rootElement);
|
|
676
|
-
}
|
|
677
|
-
state.rootElement = wrapper;
|
|
678
|
-
container.appendChild(wrapper);
|
|
679
|
-
}
|
|
680
|
-
return {
|
|
681
|
-
rootElement: state.rootElement,
|
|
682
|
-
update: (newContext) => update(state, newContext),
|
|
683
|
-
destroy: () => destroy(state)
|
|
684
|
-
};
|
|
685
1403
|
}
|
|
686
1404
|
function update(state, newContext) {
|
|
687
1405
|
state.context = newContext;
|
|
@@ -1139,6 +1857,7 @@ async function fetchComponent(id, options = {}) {
|
|
|
1139
1857
|
...DEFAULT_RETRY_CONFIG,
|
|
1140
1858
|
...retryConfig
|
|
1141
1859
|
};
|
|
1860
|
+
const startTime = performance.now();
|
|
1142
1861
|
if (!forceRefresh) {
|
|
1143
1862
|
const cached = getFromCache(id, version, cacheStrategy, cacheConfig);
|
|
1144
1863
|
if (cached) {
|
|
@@ -1146,6 +1865,11 @@ async function fetchComponent(id, options = {}) {
|
|
|
1146
1865
|
if (cached.bundle) {
|
|
1147
1866
|
registry = buildRegistryFromBundle(cached);
|
|
1148
1867
|
}
|
|
1868
|
+
const duration = performance.now() - startTime;
|
|
1869
|
+
analytics.trackFetch(id, cached.version, duration, true, {
|
|
1870
|
+
cacheHit: true,
|
|
1871
|
+
fetchDuration: Math.round(duration)
|
|
1872
|
+
});
|
|
1149
1873
|
return {
|
|
1150
1874
|
data: cached,
|
|
1151
1875
|
fromCache: true,
|
|
@@ -1192,6 +1916,12 @@ async function fetchComponent(id, options = {}) {
|
|
|
1192
1916
|
version: entry.resolved || entry.version
|
|
1193
1917
|
}));
|
|
1194
1918
|
}
|
|
1919
|
+
const duration = performance.now() - startTime;
|
|
1920
|
+
analytics.trackFetch(id, resolvedVersion, duration, false, {
|
|
1921
|
+
cacheHit: false,
|
|
1922
|
+
fetchDuration: Math.round(duration),
|
|
1923
|
+
dependencyCount: data.dependencies ? Object.keys(data.dependencies).length : 0
|
|
1924
|
+
});
|
|
1195
1925
|
return {
|
|
1196
1926
|
data,
|
|
1197
1927
|
fromCache: false,
|
|
@@ -1210,7 +1940,11 @@ async function fetchComponent(id, options = {}) {
|
|
|
1210
1940
|
}
|
|
1211
1941
|
}
|
|
1212
1942
|
}
|
|
1213
|
-
|
|
1943
|
+
const finalError = lastError || new Error("Failed to fetch component");
|
|
1944
|
+
analytics.trackError(id, version, finalError, {
|
|
1945
|
+
errorType: "fetch"
|
|
1946
|
+
});
|
|
1947
|
+
throw finalError;
|
|
1214
1948
|
}
|
|
1215
1949
|
async function fetchComponentWithDependencies(id, options = {}) {
|
|
1216
1950
|
const result = await fetchComponent(id, { ...options, includeBundle: true });
|
|
@@ -1688,8 +2422,13 @@ function getSampleValue(def) {
|
|
|
1688
2422
|
}
|
|
1689
2423
|
}
|
|
1690
2424
|
export {
|
|
2425
|
+
AnalyticsCollector,
|
|
1691
2426
|
DEFAULT_CACHE_CONFIG,
|
|
1692
2427
|
DEFAULT_RETRY_CONFIG,
|
|
2428
|
+
LongTaskObserver,
|
|
2429
|
+
MemorySampler,
|
|
2430
|
+
SessionManager,
|
|
2431
|
+
analytics,
|
|
1693
2432
|
applyStyles,
|
|
1694
2433
|
batchFetchComponents,
|
|
1695
2434
|
buildClassName,
|
|
@@ -1703,6 +2442,7 @@ export {
|
|
|
1703
2442
|
clearStyles,
|
|
1704
2443
|
collectAllDependencies,
|
|
1705
2444
|
compareVersions,
|
|
2445
|
+
configureAnalytics,
|
|
1706
2446
|
createRegistry,
|
|
1707
2447
|
detectCircularDependencies,
|
|
1708
2448
|
extractBindingKeys,
|
|
@@ -1713,11 +2453,15 @@ export {
|
|
|
1713
2453
|
formatStyleValue,
|
|
1714
2454
|
formatVersion,
|
|
1715
2455
|
generateTestCases,
|
|
2456
|
+
getAnalytics,
|
|
1716
2457
|
getCacheKey,
|
|
1717
2458
|
getDependencyTree,
|
|
1718
2459
|
getFromCache,
|
|
2460
|
+
getLongTaskObserver,
|
|
1719
2461
|
getMemoryCacheSize,
|
|
2462
|
+
getMemorySampler,
|
|
1720
2463
|
getRegistryUrl,
|
|
2464
|
+
getSessionManager,
|
|
1721
2465
|
hasTemplateSyntax,
|
|
1722
2466
|
invalidateCache,
|
|
1723
2467
|
isComponentAvailable,
|
|
@@ -1727,6 +2471,10 @@ export {
|
|
|
1727
2471
|
processStyles,
|
|
1728
2472
|
render,
|
|
1729
2473
|
renderDynamicList,
|
|
2474
|
+
resetAnalytics,
|
|
2475
|
+
resetLongTaskObserver,
|
|
2476
|
+
resetMemorySampler,
|
|
2477
|
+
resetSessionManager,
|
|
1730
2478
|
resolveBindingPath,
|
|
1731
2479
|
resolveTemplate,
|
|
1732
2480
|
resolveTemplateValue,
|