elm-ssr 0.90.0 → 0.90.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/debugger.ts +95 -59
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elm-ssr",
3
- "version": "0.90.0",
3
+ "version": "0.90.1",
4
4
  "description": "Elm-first SSR library and framework for Cloudflare Workers (and Bun): file-based routes/islands, backend-neutral effect adapters (KV/D1/Redis/Postgres), background tasks (waitUntil/Queues), SQL-file migrations, CLI scaffold + build.",
5
5
  "license": "MIT",
6
6
  "author": "Michał Majchrzak <michmajchrzak@gmail.com>",
package/src/debugger.ts CHANGED
@@ -235,38 +235,51 @@ export const injectDebugger = (html: string, data: unknown): string => {
235
235
  toggleBtn.id = 'elm-ssr-debug-toggle';
236
236
  toggleBtn.innerHTML = '⚡ Debug';
237
237
  document.body.appendChild(toggleBtn);
238
-
238
+
239
239
  // 4. Create Panel DOM
240
240
  const panel = document.createElement('div');
241
241
  panel.id = 'elm-ssr-debug-panel';
242
242
 
243
- let effectsHtml = '';
244
- if (data.effects && data.effects.length > 0) {
245
- effectsHtml = data.effects.map(eff => {
246
- const isSql = ['query', 'queryOne', 'execute'].includes(eff.kind);
247
- const sqlSnippet = isSql ? \`<pre class="debug-code">\${eff.payload.sql}</pre>\` : '';
248
- const paramsSnippet = isSql && eff.payload.params && eff.payload.params.length > 0 ?
249
- \`<div><strong>Params:</strong> \${JSON.stringify(eff.payload.params)}</div>\` : '';
250
- const outcome = eff.ok ? '<span class="status-ok">SUCCESS</span>' : '<span class="status-error">FAILED</span>';
251
- const detailError = eff.error ? \`<div class="status-error">\${eff.error}</div>\` : '';
252
- return \`
253
- <li class="debug-item">
254
- <div class="debug-item-meta" style="flex: 1;">
255
- <div style="display: flex; gap: 8px; align-items: center;">
256
- <span class="debug-badge badge-primary">\${eff.kind}</span>
257
- \${outcome}
258
- <span style="color: #9ca3af; font-size: 0.85em;">\${eff.durationMs.toFixed(1)}ms</span>
259
- </div>
260
- \${sqlSnippet}
261
- \${paramsSnippet}
262
- \${detailError}
243
+ const isSqlKind = (kind) => ['query', 'queryOne', 'execute'].includes(kind);
244
+
245
+ const generateEffectItemHtml = (eff) => {
246
+ const isSql = isSqlKind(eff.kind);
247
+ const sqlSnippet = isSql ? \`<pre class="debug-code">\${eff.payload.sql}</pre>\` : '';
248
+ const paramsSnippet = isSql && eff.payload.params && eff.payload.params.length > 0 ?
249
+ \`<div><strong>Params:</strong> \${JSON.stringify(eff.payload.params)}</div>\` : '';
250
+ const outcome = eff.ok ? '<span class="status-ok">SUCCESS</span>' : '<span class="status-error">FAILED</span>';
251
+ const detailError = eff.error ? \`<div class="status-error">\${eff.error}</div>\` : '';
252
+ return \`
253
+ <li class="debug-item">
254
+ <div class="debug-item-meta" style="flex: 1;">
255
+ <div style="display: flex; gap: 8px; align-items: center;">
256
+ <span class="debug-badge badge-primary">\${eff.kind}</span>
257
+ \${outcome}
258
+ <span style="color: #9ca3af; font-size: 0.85em;">\${eff.durationMs.toFixed(1)}ms</span>
263
259
  </div>
264
- </li>
265
- \`;
266
- }).join('');
267
- } else {
268
- effectsHtml = '<div style="color: #9ca3af; text-align: center; padding: 20px;">No server-side effects executed for this request.</div>';
269
- }
260
+ \${sqlSnippet}
261
+ \${paramsSnippet}
262
+ \${detailError}
263
+ </div>
264
+ </li>
265
+ \`;
266
+ };
267
+
268
+ const getDbHtml = (effects) => {
269
+ const dbEffects = (effects || []).filter(eff => isSqlKind(eff.kind));
270
+ if (dbEffects.length === 0) {
271
+ return '<div style="color: #9ca3af; text-align: center; padding: 20px;">No SQL database queries executed for this request.</div>';
272
+ }
273
+ return \`<ul class="debug-list">\${dbEffects.map(generateEffectItemHtml).join('')}</ul>\`;
274
+ };
275
+
276
+ const getEffectsHtml = (effects) => {
277
+ const nonDbEffects = (effects || []).filter(eff => !isSqlKind(eff.kind));
278
+ if (nonDbEffects.length === 0) {
279
+ return '<div style="color: #9ca3af; text-align: center; padding: 20px;">No other server-side effects executed for this request.</div>';
280
+ }
281
+ return \`<ul class="debug-list">\${nonDbEffects.map(generateEffectItemHtml).join('')}</ul>\`;
282
+ };
270
283
 
271
284
  panel.innerHTML = \`
272
285
  <div class="debug-header">
@@ -276,6 +289,7 @@ export const injectDebugger = (html: string, data: unknown): string => {
276
289
  <div class="debug-tabs">
277
290
  <button class="debug-tab active" data-tab="overview">Overview</button>
278
291
  <button class="debug-tab" data-tab="islands">Islands</button>
292
+ <button class="debug-tab" data-tab="database">Database</button>
279
293
  <button class="debug-tab" data-tab="effects">Effects</button>
280
294
  <button class="debug-tab" data-tab="session">Session</button>
281
295
  <button class="debug-tab" data-tab="bridges">Bridges</button>
@@ -313,11 +327,14 @@ export const injectDebugger = (html: string, data: unknown): string => {
313
327
  </ul>
314
328
  </div>
315
329
 
330
+ <!-- DATABASE TAB -->
331
+ <div class="debug-pane" id="pane-database">
332
+ \${getDbHtml(data.effects)}
333
+ </div>
334
+
316
335
  <!-- EFFECTS TAB -->
317
336
  <div class="debug-pane" id="pane-effects">
318
- <ul class="debug-list">
319
- \${effectsHtml}
320
- </ul>
337
+ \${getEffectsHtml(data.effects)}
321
338
  </div>
322
339
 
323
340
  <!-- SESSION TAB -->
@@ -379,6 +396,38 @@ export const injectDebugger = (html: string, data: unknown): string => {
379
396
  highlight.style.display = 'none';
380
397
  };
381
398
 
399
+ // Track island mutations to detect client-side state updates
400
+ const islandMutations = new Map();
401
+ const activeObservers = new Map();
402
+
403
+ const setupMutationObservers = () => {
404
+ for (const obs of activeObservers.values()) {
405
+ obs.disconnect();
406
+ }
407
+ activeObservers.clear();
408
+
409
+ const markers = Array.from(document.getElementsByTagName('elm-ssr-island'));
410
+ markers.forEach(marker => {
411
+ if (!islandMutations.has(marker)) {
412
+ islandMutations.set(marker, { count: 0, lastUpdate: null });
413
+ }
414
+
415
+ const observer = new MutationObserver(() => {
416
+ const stats = islandMutations.get(marker);
417
+ stats.count += 1;
418
+ stats.lastUpdate = new Date();
419
+
420
+ const activeTab = panel.querySelector('.debug-tab.active')?.getAttribute('data-tab');
421
+ if (activeTab === 'islands') {
422
+ scanIslands();
423
+ }
424
+ });
425
+
426
+ observer.observe(marker, { childList: true, subtree: true, characterData: true, attributes: true });
427
+ activeObservers.set(marker, observer);
428
+ });
429
+ };
430
+
382
431
  const scanIslands = () => {
383
432
  const list = panel.querySelector('#debug-islands-list');
384
433
  const markers = Array.from(document.getElementsByTagName('elm-ssr-island'));
@@ -395,6 +444,11 @@ export const injectDebugger = (html: string, data: unknown): string => {
395
444
  const isBooted = marker.getAttribute('data-elmssr-booted') === 'true';
396
445
  const id = marker.getAttribute('data-elmssr-id') || 'none';
397
446
 
447
+ const stats = islandMutations.get(marker) || { count: 0, lastUpdate: null };
448
+ const lastUpdateStr = stats.lastUpdate
449
+ ? \`Last active DOM mutation: \${stats.lastUpdate.toLocaleTimeString()}\`
450
+ : 'No client state changes recorded';
451
+
398
452
  const li = document.createElement('li');
399
453
  li.className = 'debug-item';
400
454
  li.style.cursor = 'pointer';
@@ -406,9 +460,12 @@ export const injectDebugger = (html: string, data: unknown): string => {
406
460
  \${isBooted ? 'BOOTED' : 'INITIALIZING'}
407
461
  </span>
408
462
  </div>
409
- <div style="font-size: 0.8em; margin-top: 4px; color: #9ca3af;">
463
+ <div style="font-size: 0.8em; margin-top: 4px; color: #9ca3af; display: flex; justify-content: space-between;">
410
464
  <span><strong>ID:</strong> \${id}</span>
465
+ <span style="color: #34d399;"><strong>DOM Mutations:</strong> \${stats.count}</span>
411
466
  </div>
467
+ <div style="font-size: 0.75em; color: #9ca3af; margin-top: 2px;">\${lastUpdateStr}</div>
468
+ <div style="font-size: 0.8em; margin-top: 6px; color: #e5e7eb;"><strong>Initial Props:</strong></div>
412
469
  <pre class="debug-code" style="font-size: 0.85em; max-height: 80px; overflow-y: auto;">\${JSON.stringify(JSON.parse(props), null, 2)}</pre>
413
470
  </div>
414
471
  \`;
@@ -420,6 +477,7 @@ export const injectDebugger = (html: string, data: unknown): string => {
420
477
  };
421
478
 
422
479
  panel.querySelector('[data-tab="islands"]').addEventListener('click', scanIslands);
480
+ setupMutationObservers();
423
481
 
424
482
  const bridgeLog = panel.querySelector('#debug-bridges-log');
425
483
  let firstBridge = true;
@@ -472,35 +530,11 @@ export const injectDebugger = (html: string, data: unknown): string => {
472
530
  </div>
473
531
  \`;
474
532
 
533
+ const dbPane = panel.querySelector('#pane-database');
534
+ dbPane.innerHTML = getDbHtml(newData.effects);
535
+
475
536
  const effectsPane = panel.querySelector('#pane-effects');
476
- let newEffectsHtml = '';
477
- if (newData.effects && newData.effects.length > 0) {
478
- newEffectsHtml = newData.effects.map(eff => {
479
- const isSql = ['query', 'queryOne', 'execute'].includes(eff.kind);
480
- const sqlSnippet = isSql ? \`<pre class="debug-code">\${eff.payload.sql}</pre>\` : '';
481
- const paramsSnippet = isSql && eff.payload.params && eff.payload.params.length > 0 ?
482
- \`<div><strong>Params:</strong> \${JSON.stringify(eff.payload.params)}</div>\` : '';
483
- const outcome = eff.ok ? '<span class="status-ok">SUCCESS</span>' : '<span class="status-error">FAILED</span>';
484
- const detailError = eff.error ? \`<div class="status-error">\${eff.error}</div>\` : '';
485
- return \`
486
- <li class="debug-item">
487
- <div class="debug-item-meta" style="flex: 1;">
488
- <div style="display: flex; gap: 8px; align-items: center;">
489
- <span class="debug-badge badge-primary">\${eff.kind}</span>
490
- \${outcome}
491
- <span style="color: #9ca3af; font-size: 0.85em;">\${eff.durationMs.toFixed(1)}ms</span>
492
- </div>
493
- \${sqlSnippet}
494
- \${paramsSnippet}
495
- \${detailError}
496
- </div>
497
- </li>
498
- \`;
499
- }).join('');
500
- effectsPane.innerHTML = \`<ul class="debug-list">\${newEffectsHtml}</ul>\`;
501
- } else {
502
- effectsPane.innerHTML = '<div style="color: #9ca3af; text-align: center; padding: 20px;">No server-side effects executed for this request.</div>';
503
- }
537
+ effectsPane.innerHTML = getEffectsHtml(newData.effects);
504
538
 
505
539
  const sessionPane = panel.querySelector('#pane-session');
506
540
  sessionPane.innerHTML = \`
@@ -512,6 +546,8 @@ export const injectDebugger = (html: string, data: unknown): string => {
512
546
  </div>
513
547
  \`;
514
548
 
549
+ setupMutationObservers(); // Observe any newly mounted islands
550
+
515
551
  const activeTab = panel.querySelector('.debug-tab.active').getAttribute('data-tab');
516
552
  if (activeTab === 'islands') {
517
553
  scanIslands();