edges-svelte 3.0.3 → 3.1.0

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
@@ -13,6 +13,15 @@ No context boilerplate. No hydration headaches.
13
13
 
14
14
  EdgeS is built to prevent state leaks. Its primary goal is to keep server-side state safely isolated per request while providing a clean developer experience with presenters, stores, and automatic client updates when fresh state arrives from the server. It is intentionally one-way sync from server to client and does not aim to provide full two-way state synchronization between client and server.
15
15
 
16
+ ### Sync Scope & Limitations
17
+
18
+ EdgeS does **not** aim to provide full server-to-client synchronization for all SvelteKit flows.
19
+
20
+ - The main goal of this package is **server-side state isolation per request**.
21
+ - Server-to-client sync is a convenience layer for common cases, not a strict consistency protocol.
22
+ - `svelte actions` with redirects are **not fully synchronized** by design.
23
+ - Redirect-driven flows (and similar edge cases) should be handled with explicit app-level patterns (for example cookies/session/flash state) when you need guaranteed transfer of action results across navigation.
24
+
16
25
  > Designed for **SvelteKit**.
17
26
 
18
27
  ---
@@ -1,4 +1,4 @@
1
- import { browser } from '../utils/environment.js';
1
+ import { BROWSER } from '@azure-net/tools/environment';
2
2
  import { batch } from '../utils/batch.js';
3
3
  const stateUpdateCallbacks = new Map();
4
4
  const UNDEFINED_MARKER = '__EDGES_UNDEFINED__';
@@ -43,7 +43,7 @@ const tryDecodeLegacyString = (value) => {
43
43
  }
44
44
  };
45
45
  export function registerStateUpdate(key, callback) {
46
- if (browser) {
46
+ if (BROWSER) {
47
47
  stateUpdateCallbacks.set(key, callback);
48
48
  }
49
49
  }
@@ -85,7 +85,7 @@ export function applyEdgesFromPayload(payload) {
85
85
  }
86
86
  processEdgesState(rawState);
87
87
  }
88
- if (browser) {
88
+ if (BROWSER) {
89
89
  if (!window.__EDGES_NAVIGATION_SYNC_MOUNTED__) {
90
90
  window.__EDGES_NAVIGATION_SYNC_MOUNTED__ = true;
91
91
  void Promise.all([import('svelte'), import('./NavigationStateObserver.svelte')])
@@ -1,11 +1,11 @@
1
- import { browser } from '../utils/environment.js';
1
+ import { BROWSER } from '@azure-net/tools/environment';
2
2
  class RequestContextManager {
3
3
  _currentGetter;
4
4
  init(getter) {
5
5
  this._currentGetter = getter;
6
6
  }
7
7
  current() {
8
- if (browser) {
8
+ if (BROWSER) {
9
9
  return { data: { message: 'Do not use request context on client side' } };
10
10
  }
11
11
  if (!this._currentGetter) {
@@ -1,8 +1,7 @@
1
1
  import { createState as BaseCreateState, createDerivedState as BaseCreateDerivedState, createRawState as BaseCreateRawState } from '../store/index.js';
2
+ import { BROWSER, DEV } from '@azure-net/tools/environment';
2
3
  import { RequestContext } from '../context/index.js';
3
- import { browser } from '../utils/environment.js';
4
- import { DevTools } from '../utils/dev.js';
5
- import { dev } from '../utils/environment.js';
4
+ import { recordProviderAccess, registerProviderDefinition, trackFactoryUniqueness, validateNamedProviderUniqueness } from './Utils.js';
6
5
  const globalClientCache = new Map();
7
6
  const globalConstructionStack = [];
8
7
  const PROVIDER_FACTORY_MARK = Symbol.for('edges-svelte.provider.factory');
@@ -17,7 +16,7 @@ class AutoKeyGenerator {
17
16
  cache = this.cache;
18
17
  counters = this.counters;
19
18
  };
20
- if (browser) {
19
+ if (BROWSER) {
21
20
  setGlobalCacheSystem();
22
21
  }
23
22
  else {
@@ -45,7 +44,7 @@ class AutoKeyGenerator {
45
44
  counters.set(baseKey, 0);
46
45
  }
47
46
  cache.set(factory, finalKey);
48
- DevTools.validateFactoryUniqueness(factory, finalKey);
47
+ trackFactoryUniqueness(factory, finalKey);
49
48
  return finalKey;
50
49
  }
51
50
  static hash(str) {
@@ -58,7 +57,7 @@ class AutoKeyGenerator {
58
57
  }
59
58
  }
60
59
  export const clearCache = (pattern) => {
61
- if (browser) {
60
+ if (BROWSER) {
62
61
  if (pattern) {
63
62
  for (const [key] of globalClientCache) {
64
63
  if (key.includes(pattern)) {
@@ -73,7 +72,7 @@ export const clearCache = (pattern) => {
73
72
  };
74
73
  const createUiProvider = (cacheKey, factory, dependencies, inject) => {
75
74
  const readConstructionStack = () => {
76
- if (browser)
75
+ if (BROWSER)
77
76
  return globalConstructionStack;
78
77
  try {
79
78
  const context = RequestContext.current();
@@ -89,7 +88,7 @@ const createUiProvider = (cacheKey, factory, dependencies, inject) => {
89
88
  return `[edges-svelte] Circular provider dependency detected while constructing "${key}". Chain: ${chain.join(' -> ')}.`;
90
89
  };
91
90
  const validateLazyInjection = (ownerKey, injections) => {
92
- if (!dev || !injections)
91
+ if (!DEV || !injections)
93
92
  return;
94
93
  for (const [depKey, depValue] of Object.entries(injections)) {
95
94
  if (depValue && typeof depValue === 'object') {
@@ -119,7 +118,7 @@ const createUiProvider = (cacheKey, factory, dependencies, inject) => {
119
118
  };
120
119
  const provider = (() => {
121
120
  let contextMap;
122
- if (browser) {
121
+ if (BROWSER) {
123
122
  contextMap = globalClientCache;
124
123
  }
125
124
  else {
@@ -137,6 +136,7 @@ const createUiProvider = (cacheKey, factory, dependencies, inject) => {
137
136
  if (contextMap.has(cacheKey)) {
138
137
  const cached = contextMap.get(cacheKey);
139
138
  if (cached !== undefined) {
139
+ recordProviderAccess(cacheKey, cached, BROWSER ? contextMap.size : undefined);
140
140
  return cached;
141
141
  }
142
142
  }
@@ -161,6 +161,7 @@ const createUiProvider = (cacheKey, factory, dependencies, inject) => {
161
161
  }
162
162
  markProviderInstance(instance);
163
163
  contextMap.set(cacheKey, instance);
164
+ recordProviderAccess(cacheKey, instance, BROWSER ? contextMap.size : undefined);
164
165
  return instance;
165
166
  });
166
167
  try {
@@ -181,7 +182,11 @@ export function createStore(nameOrFactory, factoryOrInject, inject) {
181
182
  const name = isNameProvided ? nameOrFactory : undefined;
182
183
  const factory = isNameProvided ? factoryOrInject : nameOrFactory;
183
184
  const injections = isNameProvided ? inject : factoryOrInject;
185
+ if (isNameProvided) {
186
+ validateNamedProviderUniqueness(name, 'store', factory);
187
+ }
184
188
  const cacheKey = name || AutoKeyGenerator.generate(factory);
189
+ registerProviderDefinition(cacheKey, 'store', factory, Boolean(name));
185
190
  return createUiProvider(cacheKey, factory, (key) => {
186
191
  let stateCounter = 0;
187
192
  return {
@@ -215,7 +220,11 @@ export function createPresenter(nameOrFactory, factoryOrInject, inject) {
215
220
  const name = isNameProvided ? nameOrFactory : undefined;
216
221
  const factory = isNameProvided ? factoryOrInject : nameOrFactory;
217
222
  const injections = isNameProvided ? inject : factoryOrInject;
223
+ if (isNameProvided) {
224
+ validateNamedProviderUniqueness(name, 'presenter', factory);
225
+ }
218
226
  const cacheKey = name || AutoKeyGenerator.generate(factory);
227
+ registerProviderDefinition(cacheKey, 'presenter', factory, Boolean(name));
219
228
  return createUiProvider(cacheKey, factory, {}, injections);
220
229
  }
221
230
  export const createPresenterFactory = (inject) => {
@@ -0,0 +1,34 @@
1
+ import type { UnknownFunc } from '../types.js';
2
+ export type ProviderKind = 'store' | 'presenter';
3
+ export interface ProviderDevDefinitionSnapshot {
4
+ key: string;
5
+ kind: ProviderKind;
6
+ factoryName: string;
7
+ named: boolean;
8
+ registeredAt: number;
9
+ }
10
+ export interface ProviderDevRuntimeSnapshot {
11
+ key: string;
12
+ instantiated: boolean;
13
+ hits: number;
14
+ lastAccessedAt: number | null;
15
+ instanceSizeBytes: number;
16
+ }
17
+ export interface ProviderDevSnapshot {
18
+ definitions: ProviderDevDefinitionSnapshot[];
19
+ runtimes: ProviderDevRuntimeSnapshot[];
20
+ providerCacheEntries: number;
21
+ providerCacheSizeBytes: number;
22
+ }
23
+ export interface ProviderDuplicateAttempt {
24
+ key: string;
25
+ attemptedKind: ProviderKind;
26
+ existingKind: ProviderKind;
27
+ at: number;
28
+ }
29
+ export declare const trackFactoryUniqueness: (factory: UnknownFunc, key: string) => void;
30
+ export declare const validateNamedProviderUniqueness: (key: string, kind: ProviderKind, factory: UnknownFunc) => void;
31
+ export declare const registerProviderDefinition: (key: string, kind: ProviderKind, factory: UnknownFunc, named: boolean) => void;
32
+ export declare const recordProviderAccess: (key: string, instance: unknown, cacheEntriesHint?: number) => void;
33
+ export declare const getProviderDevSnapshot: () => ProviderDevSnapshot;
34
+ export declare const getProviderDuplicateAttempts: () => ProviderDuplicateAttempt[];
@@ -0,0 +1,102 @@
1
+ import { BROWSER, DEV } from '@azure-net/tools/environment';
2
+ const seenFactories = new WeakSet();
3
+ const namedProviderRegistry = new Map();
4
+ const duplicateNamedProviderEvents = [];
5
+ const providerDefinitions = new Map();
6
+ const providerRuntime = new Map();
7
+ let providerCacheEntries = 0;
8
+ let providerCacheSizeBytes = 0;
9
+ const safeJsonLength = (value) => {
10
+ try {
11
+ const serialized = JSON.stringify(value);
12
+ return serialized ? serialized.length : 0;
13
+ }
14
+ catch {
15
+ return 0;
16
+ }
17
+ };
18
+ const estimateSize = (value) => {
19
+ if (typeof value === 'bigint')
20
+ return value.toString().length;
21
+ if (typeof value === 'function' || typeof value === 'symbol')
22
+ return 0;
23
+ if (typeof value === 'string')
24
+ return value.length;
25
+ if (value === undefined || value === null)
26
+ return 0;
27
+ return safeJsonLength(value);
28
+ };
29
+ export const trackFactoryUniqueness = (factory, key) => {
30
+ if (!DEV)
31
+ return;
32
+ if (seenFactories.has(factory)) {
33
+ console.warn(`[edges-svelte] Factory collision detected for key "${key}". ` +
34
+ `This might cause unexpected behavior.` +
35
+ `Set a unique __storeKey__ property on your factory function.`);
36
+ }
37
+ seenFactories.add(factory);
38
+ };
39
+ export const validateNamedProviderUniqueness = (key, kind, factory) => {
40
+ if (!DEV)
41
+ return;
42
+ const existing = namedProviderRegistry.get(key);
43
+ if (!existing) {
44
+ namedProviderRegistry.set(key, { kind, factory });
45
+ return;
46
+ }
47
+ if (existing.factory === factory && existing.kind === kind) {
48
+ return;
49
+ }
50
+ duplicateNamedProviderEvents.push({
51
+ key,
52
+ attemptedKind: kind,
53
+ existingKind: existing.kind,
54
+ at: Date.now()
55
+ });
56
+ throw new Error(`[edges-svelte] Duplicate ${kind} key "${key}" detected. ` +
57
+ `This key is already used by a ${existing.kind}. Use unique names for createStore/createPresenter.`);
58
+ };
59
+ export const registerProviderDefinition = (key, kind, factory, named) => {
60
+ if (!DEV || !BROWSER)
61
+ return;
62
+ if (providerDefinitions.has(key))
63
+ return;
64
+ providerDefinitions.set(key, {
65
+ key,
66
+ kind,
67
+ factoryName: factory.displayName || factory.name || '(anonymous)',
68
+ named,
69
+ registeredAt: Date.now()
70
+ });
71
+ };
72
+ export const recordProviderAccess = (key, instance, cacheEntriesHint) => {
73
+ if (!DEV || !BROWSER)
74
+ return;
75
+ const runtime = providerRuntime.get(key) ?? {
76
+ key,
77
+ instantiated: false,
78
+ hits: 0,
79
+ lastAccessedAt: null,
80
+ instanceSizeBytes: 0
81
+ };
82
+ runtime.instantiated = true;
83
+ runtime.hits += 1;
84
+ runtime.lastAccessedAt = Date.now();
85
+ runtime.instanceSizeBytes = estimateSize(instance);
86
+ providerRuntime.set(key, runtime);
87
+ if (typeof cacheEntriesHint === 'number') {
88
+ providerCacheEntries = cacheEntriesHint;
89
+ }
90
+ providerCacheSizeBytes = 0;
91
+ for (const [providerKey, tracked] of providerRuntime) {
92
+ providerCacheSizeBytes += providerKey.length;
93
+ providerCacheSizeBytes += tracked.instanceSizeBytes;
94
+ }
95
+ };
96
+ export const getProviderDevSnapshot = () => ({
97
+ definitions: Array.from(providerDefinitions.values()),
98
+ runtimes: Array.from(providerRuntime.values()),
99
+ providerCacheEntries,
100
+ providerCacheSizeBytes
101
+ });
102
+ export const getProviderDuplicateAttempts = () => [...duplicateNamedProviderEvents];
@@ -33,6 +33,8 @@ export const edgesHandle = async (event, callback, silentChromeDevtools = false)
33
33
  serialize: (html) => {
34
34
  if (!html)
35
35
  return html ?? '';
36
+ if (!html.includes('</body>'))
37
+ return html;
36
38
  const serialized = stateSerialize();
37
39
  if (!serialized)
38
40
  return html;
@@ -1,6 +1,6 @@
1
1
  import { getStateMap } from '../store/State.svelte.js';
2
2
  import { RequestContext } from '../context/Context.js';
3
- import { build, dev } from '../utils/environment.js';
3
+ import { DEV } from '@azure-net/tools/environment';
4
4
  const UNDEFINED_MARKER = '__EDGES_UNDEFINED__';
5
5
  const NULL_MARKER = '__EDGES_NULL__';
6
6
  const BIGINT_MARKER = '__EDGES_BIGINT__';
@@ -9,7 +9,6 @@ const EDGES_REV_FIELD = '__edges_rev__';
9
9
  const isObjectRecord = (value) => {
10
10
  return typeof value === 'object' && value !== null && !Array.isArray(value);
11
11
  };
12
- const PROFILE_EDGES_DELTA = dev && !build;
13
12
  const encodeEdgesValue = (value) => {
14
13
  if (value === undefined)
15
14
  return { [UNDEFINED_MARKER]: true };
@@ -30,7 +29,7 @@ const encodeEdgesValue = (value) => {
30
29
  return value;
31
30
  };
32
31
  const getEdgesDelta = () => {
33
- const startedAt = PROFILE_EDGES_DELTA ? performance.now() : 0;
32
+ const startedAt = DEV ? performance.now() : 0;
34
33
  try {
35
34
  const context = RequestContext.current();
36
35
  const dirtyKeys = context.data.edgesDirtyKeys;
@@ -52,7 +51,7 @@ const getEdgesDelta = () => {
52
51
  return undefined;
53
52
  }
54
53
  finally {
55
- if (PROFILE_EDGES_DELTA) {
54
+ if (DEV) {
56
55
  const duration = performance.now() - startedAt;
57
56
  if (duration > 4) {
58
57
  console.debug(`[edges-svelte] edges delta encode took ${duration.toFixed(2)}ms`);
@@ -2,7 +2,7 @@ import { type Readable, type Writable } from 'svelte/store';
2
2
  declare global {
3
3
  interface Window {
4
4
  __SAFE_SSR_STATE__?: Map<string, unknown>;
5
- __EDGES_DEVTOOLS__: Record<string, unknown>;
5
+ __EDGES_DEVTOOLS__?: Record<string, unknown>;
6
6
  }
7
7
  }
8
8
  export declare const stateSerialize: () => string;
@@ -1,5 +1,5 @@
1
1
  import { RequestContext } from '../context/Context.js';
2
- import { browser } from '../utils/environment.js';
2
+ import { BROWSER } from '@azure-net/tools/environment';
3
3
  import { derived, writable } from 'svelte/store';
4
4
  import { registerStateUpdate } from '../client/NavigationSync.svelte.js';
5
5
  import { queueUpdate, isBatching } from '../utils/batch.js';
@@ -56,7 +56,7 @@ const getRequestContext = () => {
56
56
  }
57
57
  };
58
58
  const markStateDirty = (key) => {
59
- if (browser)
59
+ if (BROWSER)
60
60
  return;
61
61
  try {
62
62
  const context = RequestContext.current();
@@ -85,7 +85,7 @@ const getBrowserState = (key, initial) => {
85
85
  return initial;
86
86
  };
87
87
  export const createRawState = (key, initial) => {
88
- if (browser) {
88
+ if (BROWSER) {
89
89
  let state = $state(getBrowserState(key, initial()));
90
90
  const updateWindowState = (val) => {
91
91
  if (!window.__SAFE_SSR_STATE__) {
@@ -138,7 +138,7 @@ export const createRawState = (key, initial) => {
138
138
  };
139
139
  };
140
140
  export const createState = (key, initial) => {
141
- if (browser) {
141
+ if (BROWSER) {
142
142
  const initialValue = getBrowserState(key, initial());
143
143
  const state = writable(initialValue);
144
144
  let currentValue = initialValue;
@@ -222,7 +222,7 @@ export const createState = (key, initial) => {
222
222
  };
223
223
  };
224
224
  export const createDerivedState = (stores, deriveFn) => {
225
- if (browser) {
225
+ if (BROWSER) {
226
226
  return derived(stores, deriveFn);
227
227
  }
228
228
  return {
@@ -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
+ }
package/dist/utils/dev.js CHANGED
@@ -1,31 +1,133 @@
1
- import { browser, dev } from './environment.js';
2
- const seenFactories = new WeakSet();
1
+ import { BROWSER, DEV } from '@azure-net/tools/environment';
2
+ import { getProviderDevSnapshot, getProviderDuplicateAttempts } from '../provider/Utils.js';
3
3
  const largeStateWarnings = new Set();
4
4
  const sizeCache = new WeakMap();
5
- export const DevTools = {
6
- validateFactoryUniqueness(factory, key) {
7
- if (!dev)
8
- return;
9
- if (seenFactories.has(factory)) {
10
- console.warn(`[edges-svelte] Factory collision detected for key "${key}". ` +
11
- `This might cause unexpected behavior.` +
12
- `Set a unique __storeKey__ property on your factory function.`);
5
+ const safeJsonLength = (value) => {
6
+ const serialized = JSON.stringify(value);
7
+ return serialized ? serialized.length : 0;
8
+ };
9
+ const parseStateKey = (fullKey) => {
10
+ const stateSeparator = '::state::';
11
+ const rawStateSeparator = '::rawstate::';
12
+ const stateIdx = fullKey.lastIndexOf(stateSeparator);
13
+ if (stateIdx !== -1) {
14
+ const providerKey = fullKey.slice(0, stateIdx);
15
+ const indexRaw = fullKey.slice(stateIdx + stateSeparator.length);
16
+ const index = Number(indexRaw);
17
+ return { providerKey, slot: 'state', index: Number.isFinite(index) ? index : null };
18
+ }
19
+ const rawStateIdx = fullKey.lastIndexOf(rawStateSeparator);
20
+ if (rawStateIdx !== -1) {
21
+ const providerKey = fullKey.slice(0, rawStateIdx);
22
+ const indexRaw = fullKey.slice(rawStateIdx + rawStateSeparator.length);
23
+ const index = Number(indexRaw);
24
+ return { providerKey, slot: 'rawstate', index: Number.isFinite(index) ? index : null };
25
+ }
26
+ return { providerKey: fullKey, slot: 'unknown', index: null };
27
+ };
28
+ const previewValue = (value) => {
29
+ if (typeof value === 'string') {
30
+ return value.length > 160 ? `${value.slice(0, 157)}...` : value;
31
+ }
32
+ if (typeof value === 'bigint') {
33
+ return `${value.toString()}n`;
34
+ }
35
+ if (typeof value === 'function') {
36
+ return '[Function]';
37
+ }
38
+ if (typeof value === 'symbol') {
39
+ return value.toString();
40
+ }
41
+ try {
42
+ const serialized = JSON.stringify(value);
43
+ if (!serialized)
44
+ return String(value);
45
+ return serialized.length > 160 ? `${serialized.slice(0, 157)}...` : serialized;
46
+ }
47
+ catch {
48
+ return Object.prototype.toString.call(value);
49
+ }
50
+ };
51
+ const getStoreStateEntries = (stateMap) => {
52
+ const grouped = new Map();
53
+ for (const [fullKey, value] of stateMap) {
54
+ const parsed = parseStateKey(fullKey);
55
+ const entry = {
56
+ fullKey,
57
+ slot: parsed.slot,
58
+ index: parsed.index,
59
+ sizeBytes: DevTools.getSize(value),
60
+ valuePreview: previewValue(value)
61
+ };
62
+ const existing = grouped.get(parsed.providerKey);
63
+ if (existing) {
64
+ existing.push(entry);
13
65
  }
14
- seenFactories.add(factory);
15
- },
66
+ else {
67
+ grouped.set(parsed.providerKey, [entry]);
68
+ }
69
+ }
70
+ for (const entries of grouped.values()) {
71
+ entries.sort((a, b) => {
72
+ if (a.slot !== b.slot)
73
+ return a.slot.localeCompare(b.slot);
74
+ if (a.index === null && b.index === null)
75
+ return a.fullKey.localeCompare(b.fullKey);
76
+ if (a.index === null)
77
+ return 1;
78
+ if (b.index === null)
79
+ return -1;
80
+ return a.index - b.index;
81
+ });
82
+ }
83
+ return grouped;
84
+ };
85
+ const buildProviderSnapshot = (def, runtime, stateEntries) => {
86
+ const states = stateEntries.get(def.key) ?? [];
87
+ const stateSizeBytes = states.reduce((sum, entry) => sum + entry.sizeBytes, 0);
88
+ return {
89
+ key: def.key,
90
+ kind: def.kind,
91
+ factoryName: def.factoryName,
92
+ named: def.named,
93
+ registeredAt: def.registeredAt,
94
+ instantiated: runtime?.instantiated ?? false,
95
+ hits: runtime?.hits ?? 0,
96
+ lastAccessedAt: runtime?.lastAccessedAt ?? null,
97
+ instanceSizeBytes: runtime?.instanceSizeBytes ?? 0,
98
+ stateCount: states.length,
99
+ stateSizeBytes,
100
+ states
101
+ };
102
+ };
103
+ const emptyInspectorSnapshot = () => ({
104
+ presenters: [],
105
+ stores: [],
106
+ info: {
107
+ totalPresenters: 0,
108
+ totalStores: 0,
109
+ totalProviders: 0,
110
+ instantiatedProviders: 0,
111
+ totalStateEntries: 0,
112
+ totalStateSizeBytes: 0,
113
+ providerCacheEntries: 0,
114
+ providerCacheSizeBytes: 0
115
+ }
116
+ });
117
+ export const DevTools = {
16
118
  getSize(value) {
17
119
  if (typeof value === 'object' && value !== null) {
18
120
  if (sizeCache.has(value)) {
19
121
  return sizeCache.get(value);
20
122
  }
21
- const size = JSON.stringify(value).length;
123
+ const size = safeJsonLength(value);
22
124
  sizeCache.set(value, size);
23
125
  return size;
24
126
  }
25
- return JSON.stringify(value).length;
127
+ return safeJsonLength(value);
26
128
  },
27
129
  warnOnLargeState(key, value) {
28
- if (!dev)
130
+ if (!DEV)
29
131
  return;
30
132
  if (largeStateWarnings.has(key))
31
133
  return;
@@ -41,7 +143,7 @@ export const DevTools = {
41
143
  }
42
144
  },
43
145
  checkStateMutation(key, oldValue, newValue) {
44
- if (!dev || !browser)
146
+ if (!DEV || !BROWSER)
45
147
  return;
46
148
  if (typeof oldValue === 'object' && oldValue !== null && oldValue === newValue && !Array.isArray(oldValue)) {
47
149
  console.error(`[edges-svelte] Direct mutation detected for key "${key}". ` +
@@ -49,7 +151,7 @@ export const DevTools = {
49
151
  }
50
152
  },
51
153
  measurePerformance(name, fn) {
52
- if (!dev)
154
+ if (!DEV)
53
155
  return fn();
54
156
  const start = performance.now();
55
157
  const result = fn();
@@ -60,7 +162,7 @@ export const DevTools = {
60
162
  return result;
61
163
  },
62
164
  visualizeStateTree(stateMap) {
63
- if (!dev || !browser)
165
+ if (!DEV || !BROWSER)
64
166
  return;
65
167
  const tree = {};
66
168
  for (const [key, value] of stateMap) {
@@ -77,11 +179,70 @@ export const DevTools = {
77
179
  console.groupCollapsed('[edges-svelte] State Tree');
78
180
  console.dir(tree, { depth: null });
79
181
  console.groupEnd();
182
+ },
183
+ getInspectorData() {
184
+ if (!BROWSER)
185
+ return emptyInspectorSnapshot();
186
+ const stateMap = window.__SAFE_SSR_STATE__ ?? new Map();
187
+ const groupedStateEntries = getStoreStateEntries(stateMap);
188
+ const providerSnapshot = getProviderDevSnapshot();
189
+ const runtimeByKey = new Map(providerSnapshot.runtimes.map((runtime) => [runtime.key, runtime]));
190
+ const providerDefinitionKeys = new Set(providerSnapshot.definitions.map((def) => def.key));
191
+ const snapshots = [];
192
+ for (const def of providerSnapshot.definitions) {
193
+ snapshots.push(buildProviderSnapshot(def, runtimeByKey.get(def.key), groupedStateEntries));
194
+ }
195
+ for (const [providerKey, states] of groupedStateEntries) {
196
+ if (providerDefinitionKeys.has(providerKey))
197
+ continue;
198
+ snapshots.push({
199
+ key: providerKey,
200
+ kind: 'store',
201
+ factoryName: '(state-only)',
202
+ named: false,
203
+ registeredAt: 0,
204
+ instantiated: false,
205
+ hits: 0,
206
+ lastAccessedAt: null,
207
+ instanceSizeBytes: 0,
208
+ stateCount: states.length,
209
+ stateSizeBytes: states.reduce((sum, entry) => sum + entry.sizeBytes, 0),
210
+ states
211
+ });
212
+ }
213
+ snapshots.sort((a, b) => a.key.localeCompare(b.key));
214
+ const presenters = snapshots.filter((entry) => entry.kind === 'presenter');
215
+ const stores = snapshots.filter((entry) => entry.kind === 'store');
216
+ const totalStateSizeBytes = stores.reduce((sum, store) => sum + store.stateSizeBytes, 0);
217
+ const totalStateEntries = stores.reduce((sum, store) => sum + store.stateCount, 0);
218
+ const instantiatedProviders = snapshots.filter((entry) => entry.instantiated).length;
219
+ return {
220
+ presenters,
221
+ stores,
222
+ info: {
223
+ totalPresenters: presenters.length,
224
+ totalStores: stores.length,
225
+ totalProviders: snapshots.length,
226
+ instantiatedProviders,
227
+ totalStateEntries,
228
+ totalStateSizeBytes,
229
+ providerCacheEntries: providerSnapshot.providerCacheEntries,
230
+ providerCacheSizeBytes: providerSnapshot.providerCacheSizeBytes
231
+ }
232
+ };
233
+ },
234
+ checkKeyUniqueness() {
235
+ const duplicateAttempts = getProviderDuplicateAttempts();
236
+ return {
237
+ ok: duplicateAttempts.length === 0,
238
+ providerKeysChecked: getProviderDevSnapshot().definitions.length,
239
+ duplicateAttempts
240
+ };
80
241
  }
81
242
  };
82
- if (browser && dev) {
83
- window.__EDGES_DEVTOOLS__ = {
84
- version: '1.3.0',
243
+ if (BROWSER && DEV) {
244
+ const api = {
245
+ version: '1.4.0',
85
246
  visualizeState: () => {
86
247
  const stateMap = window.__SAFE_SSR_STATE__;
87
248
  if (stateMap) {
@@ -113,7 +274,28 @@ if (browser && dev) {
113
274
  totalSize: `${Math.round(totalSize / 1024)}KB`,
114
275
  storeSizes: sizes
115
276
  };
116
- }
277
+ },
278
+ getInspectorData: () => DevTools.getInspectorData(),
279
+ checkKeyUniqueness: () => DevTools.checkKeyUniqueness()
117
280
  };
281
+ window.__EDGES_DEVTOOLS__ = api;
282
+ if (!window.__EDGES_DEVTOOLS_UI_MOUNTED__) {
283
+ window.__EDGES_DEVTOOLS_UI_MOUNTED__ = true;
284
+ void Promise.all([import('svelte'), import('./DevTools.svelte')])
285
+ .then(([svelte, module]) => {
286
+ const target = document.body || document.documentElement;
287
+ const host = document.createElement('div');
288
+ host.setAttribute('data-edges-devtools-ui', '1');
289
+ target.appendChild(host);
290
+ svelte.mount(module.default, { target: host });
291
+ window.addEventListener('beforeunload', () => {
292
+ host.remove();
293
+ window.__EDGES_DEVTOOLS_UI_MOUNTED__ = false;
294
+ }, { once: true });
295
+ })
296
+ .catch(() => {
297
+ window.__EDGES_DEVTOOLS_UI_MOUNTED__ = false;
298
+ });
299
+ }
118
300
  console.log('%c[edges-svelte] DevTools enabled. Use window.__EDGES_DEVTOOLS__ for debugging.', 'color: #00bcd4; font-weight: bold');
119
301
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edges-svelte",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
4
4
  "license": "MIT",
5
5
  "author": "Pixel1917",
6
6
  "description": "A blazing-fast, extremely lightweight and SSR-friendly store for Svelte",
@@ -53,44 +53,46 @@
53
53
  "./dev": {
54
54
  "types": "./dist/utils/dev.d.ts",
55
55
  "default": "./dist/utils/dev.js"
56
+ },
57
+ "./dev-component": {
58
+ "types": "./dist/utils/DevTools.svelte.d.ts",
59
+ "default": "./dist/utils/DevTools.svelte"
56
60
  }
57
61
  },
58
62
  "peerDependencies": {
59
- "svelte": "^5.0.0",
60
- "@sveltejs/kit": "^2.16.0"
63
+ "@sveltejs/kit": "2.57.1",
64
+ "svelte": "5.53.5"
61
65
  },
62
66
  "devDependencies": {
63
- "@eslint/compat": "^1.2.5",
64
- "@eslint/js": "^9.18.0",
65
- "@playwright/test": "^1.49.1",
66
- "@sveltejs/adapter-auto": "^6.0.0",
67
- "@sveltejs/kit": "^2.16.0",
68
- "@sveltejs/package": "^2.0.0",
69
- "@sveltejs/vite-plugin-svelte": "^5.0.0",
70
- "@testing-library/jest-dom": "^6.6.3",
71
- "@testing-library/svelte": "^5.2.4",
72
- "@types/node": "^22.15.23",
73
- "eslint": "^9.18.0",
74
- "eslint-config-prettier": "^10.0.1",
75
- "eslint-plugin-svelte": "^3.0.0",
76
- "globals": "^16.0.0",
77
- "jsdom": "^26.0.0",
78
- "prettier": "^3.4.2",
79
- "prettier-plugin-svelte": "^3.3.3",
80
- "publint": "^0.3.2",
81
- "svelte": "^5.0.0",
82
- "svelte-check": "^4.0.0",
83
- "typescript": "^5.0.0",
84
- "typescript-eslint": "^8.20.0",
85
- "vite": "^6.2.6",
86
- "vitest": "^3.0.0",
87
- "semantic-release": "^24.2.3",
88
- "husky": "^9.1.7",
89
- "git-cz": "^4.9.0",
67
+ "@commitlint/cli": "^20.5.0",
68
+ "@eslint/compat": "^2.0.5",
69
+ "@eslint/js": "^10.0.1",
70
+ "@playwright/test": "^1.59.1",
71
+ "@sveltejs/adapter-auto": "^7.0.1",
72
+ "@sveltejs/kit": "2.57.1",
73
+ "@sveltejs/package": "^2.5.7",
74
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
75
+ "@testing-library/jest-dom": "^6.9.1",
76
+ "@testing-library/svelte": "^5.3.1",
77
+ "@types/node": "^25.6.0",
90
78
  "commitizen": "^4.3.1",
91
- "env-cmd": "^10.1.0",
92
- "@semantic-release/git": "^10.0.1",
93
- "@commitlint/cli": "^19.7.1"
79
+ "env-cmd": "^11.0.0",
80
+ "eslint": "^10.2.1",
81
+ "eslint-config-prettier": "^10.1.8",
82
+ "eslint-plugin-svelte": "^3.17.0",
83
+ "git-cz": "^4.9.0",
84
+ "globals": "^17.5.0",
85
+ "husky": "^9.1.7",
86
+ "jsdom": "^29.0.2",
87
+ "prettier": "^3.8.3",
88
+ "prettier-plugin-svelte": "^3.5.1",
89
+ "publint": "^0.3.18",
90
+ "svelte": "^5.53.5",
91
+ "svelte-check": "^4.4.6",
92
+ "typescript": "^5.0.0",
93
+ "typescript-eslint": "^8.58.2",
94
+ "vite": "^8.0.5",
95
+ "vitest": "^4.1.4"
94
96
  },
95
97
  "config": {
96
98
  "commitizen": {
@@ -107,6 +109,9 @@
107
109
  "state management",
108
110
  "ssr store"
109
111
  ],
112
+ "dependencies": {
113
+ "@azure-net/tools": "^1.1.3"
114
+ },
110
115
  "scripts": {
111
116
  "dev": "vite dev",
112
117
  "build": "vite build && npm run prepack",
@@ -1,3 +0,0 @@
1
- export declare const browser: boolean;
2
- export declare const dev: boolean;
3
- export declare const build: boolean;
@@ -1,3 +0,0 @@
1
- export const browser = !import.meta.env.SSR && typeof window !== 'undefined' && typeof document !== 'undefined';
2
- export const dev = import.meta.env.DEV;
3
- export const build = import.meta.env.PROD;