edges-svelte 3.0.3 → 3.1.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.
@@ -0,0 +1,388 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import type { DevToolsInspectorSnapshot, DevToolsKeyCheckResult, EdgesDevtoolsWindowApi } from './dev.js';
4
+
5
+ type Tab = 'presenters' | 'stores' | 'info';
6
+
7
+ const emptySnapshot = (): DevToolsInspectorSnapshot => ({
8
+ presenters: [],
9
+ stores: [],
10
+ info: {
11
+ totalPresenters: 0,
12
+ totalStores: 0,
13
+ totalProviders: 0,
14
+ instantiatedProviders: 0,
15
+ totalStateEntries: 0,
16
+ totalStateSizeBytes: 0,
17
+ providerCacheEntries: 0,
18
+ providerCacheSizeBytes: 0
19
+ }
20
+ });
21
+
22
+ let isOpen = false;
23
+ let activeTab: Tab = 'presenters';
24
+ let inspector: DevToolsInspectorSnapshot = emptySnapshot();
25
+ let keyCheckResult: DevToolsKeyCheckResult | null = null;
26
+ let presenterExpanded = new Set<string>();
27
+ let storeExpanded = new Set<string>();
28
+
29
+ const toKb = (value: number) => `${(value / 1024).toFixed(2)} KB`;
30
+ const toDateTime = (ts: number | null) => (ts ? new Date(ts).toLocaleTimeString() : 'never');
31
+
32
+ const refresh = () => {
33
+ const api = window.__EDGES_DEVTOOLS__ as EdgesDevtoolsWindowApi | undefined;
34
+ inspector = api?.getInspectorData?.() ?? emptySnapshot();
35
+ };
36
+
37
+ const toggleExpanded = (target: Set<string>, key: string) => {
38
+ const next = new Set(target);
39
+ if (next.has(key)) next.delete(key);
40
+ else next.add(key);
41
+ return next;
42
+ };
43
+
44
+ const runKeyCheck = () => {
45
+ const api = window.__EDGES_DEVTOOLS__ as EdgesDevtoolsWindowApi | undefined;
46
+ keyCheckResult = api?.checkKeyUniqueness?.() ?? null;
47
+ };
48
+
49
+ const clearStateCache = () => {
50
+ const api = window.__EDGES_DEVTOOLS__ as EdgesDevtoolsWindowApi | undefined;
51
+ api?.clearCache?.();
52
+ refresh();
53
+ };
54
+
55
+ onMount(() => {
56
+ refresh();
57
+ const interval = window.setInterval(() => {
58
+ if (isOpen) refresh();
59
+ }, 1500);
60
+ return () => window.clearInterval(interval);
61
+ });
62
+ </script>
63
+
64
+ <div class="edges-devtools">
65
+ <button class="launcher" on:click={() => (isOpen = !isOpen)} aria-label="Toggle edges devtools"> DEV </button>
66
+
67
+ {#if isOpen}
68
+ <div class="panel">
69
+ <div class="tabs">
70
+ <button class:active={activeTab === 'presenters'} on:click={() => (activeTab = 'presenters')}
71
+ >presenters ({inspector.presenters.length})</button
72
+ >
73
+ <button class:active={activeTab === 'stores'} on:click={() => (activeTab = 'stores')}>stores ({inspector.stores.length})</button>
74
+ <button class:active={activeTab === 'info'} on:click={() => (activeTab = 'info')}>info</button>
75
+ </div>
76
+
77
+ <div class="content">
78
+ {#if activeTab === 'presenters'}
79
+ {#if inspector.presenters.length === 0}
80
+ <div class="empty">No presenters registered yet.</div>
81
+ {:else}
82
+ {#each inspector.presenters as presenter (presenter)}
83
+ <div class="accordion-item">
84
+ <button class="accordion-head" on:click={() => (presenterExpanded = toggleExpanded(presenterExpanded, presenter.key))}>
85
+ <span>{presenter.key}</span>
86
+ <span>{presenterExpanded.has(presenter.key) ? '−' : '+'}</span>
87
+ </button>
88
+ {#if presenterExpanded.has(presenter.key)}
89
+ <div class="accordion-body">
90
+ <div><strong>Factory:</strong> {presenter.factoryName}</div>
91
+ <div><strong>Named key:</strong> {presenter.named ? 'yes' : 'no (auto-generated)'}</div>
92
+ <div><strong>Instantiated:</strong> {presenter.instantiated ? 'yes' : 'no'}</div>
93
+ <div><strong>Hits:</strong> {presenter.hits}</div>
94
+ <div><strong>Instance size:</strong> {toKb(presenter.instanceSizeBytes)}</div>
95
+ <div><strong>Last access:</strong> {toDateTime(presenter.lastAccessedAt)}</div>
96
+ <div><strong>Registered:</strong> {toDateTime(presenter.registeredAt)}</div>
97
+ </div>
98
+ {/if}
99
+ </div>
100
+ {/each}
101
+ {/if}
102
+ {/if}
103
+
104
+ {#if activeTab === 'stores'}
105
+ {#if inspector.stores.length === 0}
106
+ <div class="empty">No stores registered yet.</div>
107
+ {:else}
108
+ {#each inspector.stores as store (store)}
109
+ <div class="accordion-item">
110
+ <button class="accordion-head" on:click={() => (storeExpanded = toggleExpanded(storeExpanded, store.key))}>
111
+ <span>{store.key}</span>
112
+ <span>{storeExpanded.has(store.key) ? '−' : '+'}</span>
113
+ </button>
114
+ {#if storeExpanded.has(store.key)}
115
+ <div class="accordion-body">
116
+ <div><strong>Factory:</strong> {store.factoryName}</div>
117
+ <div><strong>Named key:</strong> {store.named ? 'yes' : 'no (auto-generated)'}</div>
118
+ <div><strong>States:</strong> {store.stateCount}</div>
119
+ <div><strong>Total state size:</strong> {toKb(store.stateSizeBytes)}</div>
120
+ <div><strong>Instance size:</strong> {toKb(store.instanceSizeBytes)}</div>
121
+ <div><strong>Last access:</strong> {toDateTime(store.lastAccessedAt)}</div>
122
+ <div class="state-tree-title">State tree</div>
123
+ {#if store.states.length === 0}
124
+ <div class="empty tiny">No state entries found for this store.</div>
125
+ {:else}
126
+ <div class="state-tree">
127
+ {#each store.states as stateEntry (stateEntry)}
128
+ <div class="state-entry">
129
+ <div><strong>{stateEntry.fullKey}</strong></div>
130
+ <div>slot: {stateEntry.slot}{stateEntry.index !== null ? ` #${stateEntry.index}` : ''}</div>
131
+ <div>size: {toKb(stateEntry.sizeBytes)}</div>
132
+ <div class="preview">value: {stateEntry.valuePreview}</div>
133
+ </div>
134
+ {/each}
135
+ </div>
136
+ {/if}
137
+ </div>
138
+ {/if}
139
+ </div>
140
+ {/each}
141
+ {/if}
142
+ {/if}
143
+
144
+ {#if activeTab === 'info'}
145
+ <div class="info-actions">
146
+ <button on:click={refresh}>Refresh snapshot</button>
147
+ <button on:click={runKeyCheck}>Check key uniqueness</button>
148
+ <button on:click={clearStateCache}>Clear state cache</button>
149
+ </div>
150
+ <div class="metrics">
151
+ <div><strong>Total providers:</strong> {inspector.info.totalProviders}</div>
152
+ <div><strong>Presenters:</strong> {inspector.info.totalPresenters}</div>
153
+ <div><strong>Stores:</strong> {inspector.info.totalStores}</div>
154
+ <div><strong>Instantiated providers:</strong> {inspector.info.instantiatedProviders}</div>
155
+ <div><strong>Total state entries:</strong> {inspector.info.totalStateEntries}</div>
156
+ <div><strong>Total state size:</strong> {toKb(inspector.info.totalStateSizeBytes)}</div>
157
+ <div><strong>Provider cache entries:</strong> {inspector.info.providerCacheEntries}</div>
158
+ <div><strong>Provider cache size:</strong> {toKb(inspector.info.providerCacheSizeBytes)}</div>
159
+ </div>
160
+
161
+ {#if keyCheckResult}
162
+ <div class="key-check {keyCheckResult.ok ? 'ok' : 'warn'}">
163
+ <div><strong>Key check:</strong> {keyCheckResult.ok ? 'OK' : 'Issues found'}</div>
164
+ <div><strong>Provider keys checked:</strong> {keyCheckResult.providerKeysChecked}</div>
165
+ {#if keyCheckResult.duplicateAttempts.length > 0}
166
+ <div class="duplicate-list">
167
+ {#each keyCheckResult.duplicateAttempts as duplicate (duplicate)}
168
+ <div>
169
+ {duplicate.key}: attempted {duplicate.attemptedKind}, existing {duplicate.existingKind} ({toDateTime(duplicate.at)})
170
+ </div>
171
+ {/each}
172
+ </div>
173
+ {/if}
174
+ </div>
175
+ {/if}
176
+ {/if}
177
+ </div>
178
+ </div>
179
+ {/if}
180
+ </div>
181
+
182
+ <style>
183
+ .edges-devtools {
184
+ position: fixed;
185
+ right: 20px;
186
+ bottom: 20px;
187
+ z-index: 999999;
188
+ font-family: 'IBM Plex Sans', 'Segoe UI', sans-serif;
189
+ }
190
+
191
+ .launcher {
192
+ width: 50px;
193
+ height: 50px;
194
+ border-radius: 999px;
195
+ border: none;
196
+ background: linear-gradient(140deg, #0f766e, #155e75);
197
+ color: #f8fafc;
198
+ font-weight: 700;
199
+ letter-spacing: 0.04em;
200
+ cursor: pointer;
201
+ box-shadow: 0 8px 18px rgba(15, 118, 110, 0.45);
202
+ }
203
+
204
+ .panel {
205
+ position: absolute;
206
+ right: 0;
207
+ bottom: 62px;
208
+ width: min(760px, calc(100vw - 32px));
209
+ height: min(520px, calc(100vh - 96px));
210
+ display: grid;
211
+ grid-template-columns: 180px 1fr;
212
+ background: #f8fafc;
213
+ border: 1px solid #cbd5e1;
214
+ border-radius: 14px;
215
+ overflow: hidden;
216
+ box-shadow: 0 20px 50px rgba(15, 23, 42, 0.25);
217
+ }
218
+
219
+ .tabs {
220
+ display: flex;
221
+ flex-direction: column;
222
+ gap: 6px;
223
+ padding: 12px;
224
+ background: #e2e8f0;
225
+ border-right: 1px solid #cbd5e1;
226
+ }
227
+
228
+ .tabs button {
229
+ text-align: left;
230
+ border: 1px solid #94a3b8;
231
+ background: #f8fafc;
232
+ padding: 8px;
233
+ border-radius: 8px;
234
+ cursor: pointer;
235
+ font-size: 12px;
236
+ font-weight: 600;
237
+ text-transform: lowercase;
238
+ }
239
+
240
+ .tabs button.active {
241
+ background: #0f766e;
242
+ color: #f8fafc;
243
+ border-color: #0f766e;
244
+ }
245
+
246
+ .content {
247
+ padding: 12px;
248
+ overflow: auto;
249
+ font-size: 12px;
250
+ color: #0f172a;
251
+ display: flex;
252
+ flex-direction: column;
253
+ gap: 10px;
254
+ min-height: 0;
255
+ }
256
+
257
+ .accordion-item {
258
+ border: 1px solid #cbd5e1;
259
+ border-radius: 10px;
260
+ overflow: hidden;
261
+ display: flex;
262
+ flex-direction: column;
263
+ min-height: 0;
264
+ }
265
+
266
+ .accordion-head {
267
+ width: 100%;
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: space-between;
271
+ padding: 9px 10px;
272
+ font-weight: 700;
273
+ background: #f1f5f9;
274
+ border: none;
275
+ cursor: pointer;
276
+ }
277
+
278
+ .accordion-body {
279
+ display: flex;
280
+ flex-direction: column;
281
+ gap: 4px;
282
+ padding: 10px;
283
+ background: #ffffff;
284
+ max-height: 240px;
285
+ overflow: auto;
286
+ min-height: 0;
287
+ }
288
+
289
+ .state-tree-title {
290
+ margin-top: 6px;
291
+ font-weight: 700;
292
+ }
293
+
294
+ .state-tree {
295
+ display: flex;
296
+ flex-direction: column;
297
+ gap: 8px;
298
+ max-height: 180px;
299
+ overflow: auto;
300
+ padding-right: 2px;
301
+ }
302
+
303
+ .state-entry {
304
+ border: 1px solid #dbeafe;
305
+ background: #f8fbff;
306
+ border-radius: 8px;
307
+ padding: 7px;
308
+ }
309
+
310
+ .preview {
311
+ font-family: 'IBM Plex Mono', 'SFMono-Regular', ui-monospace, monospace;
312
+ word-break: break-word;
313
+ }
314
+
315
+ .empty {
316
+ padding: 12px;
317
+ border: 1px dashed #94a3b8;
318
+ border-radius: 8px;
319
+ color: #475569;
320
+ }
321
+
322
+ .tiny {
323
+ padding: 6px;
324
+ }
325
+
326
+ .info-actions {
327
+ display: flex;
328
+ gap: 8px;
329
+ flex-wrap: wrap;
330
+ }
331
+
332
+ .info-actions button {
333
+ border: 1px solid #64748b;
334
+ background: #ffffff;
335
+ padding: 6px 8px;
336
+ border-radius: 8px;
337
+ cursor: pointer;
338
+ font-size: 12px;
339
+ }
340
+
341
+ .metrics {
342
+ display: grid;
343
+ grid-template-columns: repeat(2, minmax(0, 1fr));
344
+ gap: 6px;
345
+ padding: 8px;
346
+ background: #f8fafc;
347
+ border: 1px solid #cbd5e1;
348
+ border-radius: 8px;
349
+ }
350
+
351
+ .key-check {
352
+ padding: 8px;
353
+ border-radius: 8px;
354
+ border: 1px solid;
355
+ }
356
+
357
+ .key-check.ok {
358
+ border-color: #16a34a;
359
+ background: #f0fdf4;
360
+ }
361
+
362
+ .key-check.warn {
363
+ border-color: #dc2626;
364
+ background: #fef2f2;
365
+ }
366
+
367
+ .duplicate-list {
368
+ margin-top: 6px;
369
+ display: flex;
370
+ flex-direction: column;
371
+ gap: 4px;
372
+ }
373
+
374
+ @media (max-width: 760px) {
375
+ .panel {
376
+ grid-template-columns: 1fr;
377
+ height: min(78vh, 560px);
378
+ }
379
+
380
+ .tabs {
381
+ flex-direction: row;
382
+ padding: 8px;
383
+ border-right: none;
384
+ border-bottom: 1px solid #cbd5e1;
385
+ overflow-x: auto;
386
+ }
387
+ }
388
+ </style>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const DevTools: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type DevTools = InstanceType<typeof DevTools>;
18
+ export default DevTools;
@@ -1,10 +1,10 @@
1
- import { browser } from './environment.js';
1
+ import { BROWSER } from '@azure-net/tools/environment';
2
2
  class BatchManager {
3
3
  pendingUpdates = new Map();
4
4
  depth = 0;
5
5
  batchCallbacks = new Set();
6
6
  batch(fn) {
7
- if (!browser) {
7
+ if (!BROWSER) {
8
8
  fn();
9
9
  return;
10
10
  }
@@ -20,12 +20,12 @@ class BatchManager {
20
20
  }
21
21
  }
22
22
  begin() {
23
- if (!browser)
23
+ if (!BROWSER)
24
24
  return;
25
25
  this.depth += 1;
26
26
  }
27
27
  end() {
28
- if (!browser)
28
+ if (!BROWSER)
29
29
  return;
30
30
  if (this.depth === 0)
31
31
  return;
@@ -35,10 +35,10 @@ class BatchManager {
35
35
  }
36
36
  }
37
37
  isBatching() {
38
- return browser && this.depth > 0;
38
+ return BROWSER && this.depth > 0;
39
39
  }
40
40
  queueUpdate(key, value, callback) {
41
- if (!browser || this.depth === 0) {
41
+ if (!BROWSER || this.depth === 0) {
42
42
  if (callback)
43
43
  callback(value);
44
44
  return;
@@ -73,7 +73,7 @@ export const queueUpdate = (key, value, callback) => batchManager.queueUpdate(ke
73
73
  export const onBatchComplete = (callback) => batchManager.onBatchComplete(callback);
74
74
  export const isBatching = () => batchManager.isBatching();
75
75
  export const transaction = async (fn) => {
76
- if (!browser) {
76
+ if (!BROWSER) {
77
77
  return fn();
78
78
  }
79
79
  batchManager.begin();
@@ -1,9 +1,75 @@
1
- import type { UnknownFunc } from '../types.js';
1
+ import { type ProviderKind } from '../provider/Utils.js';
2
+ export interface DevToolsStateEntrySnapshot {
3
+ fullKey: string;
4
+ slot: 'state' | 'rawstate' | 'unknown';
5
+ index: number | null;
6
+ sizeBytes: number;
7
+ valuePreview: string;
8
+ }
9
+ export interface DevToolsProviderSnapshot {
10
+ key: string;
11
+ kind: ProviderKind;
12
+ factoryName: string;
13
+ named: boolean;
14
+ registeredAt: number;
15
+ instantiated: boolean;
16
+ hits: number;
17
+ lastAccessedAt: number | null;
18
+ instanceSizeBytes: number;
19
+ stateCount: number;
20
+ stateSizeBytes: number;
21
+ states: DevToolsStateEntrySnapshot[];
22
+ }
23
+ export interface DevToolsInfoSnapshot {
24
+ totalPresenters: number;
25
+ totalStores: number;
26
+ totalProviders: number;
27
+ instantiatedProviders: number;
28
+ totalStateEntries: number;
29
+ totalStateSizeBytes: number;
30
+ providerCacheEntries: number;
31
+ providerCacheSizeBytes: number;
32
+ }
33
+ export interface DevToolsInspectorSnapshot {
34
+ presenters: DevToolsProviderSnapshot[];
35
+ stores: DevToolsProviderSnapshot[];
36
+ info: DevToolsInfoSnapshot;
37
+ }
38
+ export interface DevToolsKeyCheckResult {
39
+ ok: boolean;
40
+ providerKeysChecked: number;
41
+ duplicateAttempts: Array<{
42
+ key: string;
43
+ attemptedKind: ProviderKind;
44
+ existingKind: ProviderKind;
45
+ at: number;
46
+ }>;
47
+ }
48
+ export interface EdgesDevtoolsWindowApi {
49
+ version: string;
50
+ visualizeState: () => void;
51
+ clearCache: () => void;
52
+ getStats: () => {
53
+ totalStores: number;
54
+ totalSize: string;
55
+ storeSizes: Record<string, number>;
56
+ } | null;
57
+ getInspectorData: () => DevToolsInspectorSnapshot;
58
+ checkKeyUniqueness: () => DevToolsKeyCheckResult;
59
+ }
2
60
  export declare const DevTools: {
3
- validateFactoryUniqueness(factory: UnknownFunc, key: string): void;
4
61
  getSize(value: unknown): number;
5
62
  warnOnLargeState(key: string, value: unknown): void;
6
63
  checkStateMutation(key: string, oldValue: unknown, newValue: unknown): void;
7
64
  measurePerformance<T>(name: string, fn: () => T): T;
8
65
  visualizeStateTree(stateMap: Map<string, unknown>): void;
66
+ getInspectorData(): DevToolsInspectorSnapshot;
67
+ checkKeyUniqueness(): DevToolsKeyCheckResult;
9
68
  };
69
+ declare global {
70
+ interface Window {
71
+ __SAFE_SSR_STATE__?: Map<string, unknown>;
72
+ __EDGES_DEVTOOLS__?: Record<string, unknown>;
73
+ __EDGES_DEVTOOLS_UI_MOUNTED__?: boolean;
74
+ }
75
+ }