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 +9 -0
- package/dist/client/NavigationSync.svelte.js +3 -3
- package/dist/context/Context.js +2 -2
- package/dist/provider/Provider.js +18 -9
- package/dist/provider/Utils.d.ts +34 -0
- package/dist/provider/Utils.js +102 -0
- package/dist/server/EdgesHandle.js +2 -0
- package/dist/server/ServerSync.js +3 -4
- package/dist/store/State.svelte.d.ts +1 -1
- package/dist/store/State.svelte.js +5 -5
- package/dist/utils/DevTools.svelte +388 -0
- package/dist/utils/DevTools.svelte.d.ts +18 -0
- package/dist/utils/batch.js +7 -7
- package/dist/utils/dev.d.ts +68 -2
- package/dist/utils/dev.js +204 -22
- package/package.json +38 -33
- package/dist/utils/environment.d.ts +0 -3
- package/dist/utils/environment.js +0 -3
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 {
|
|
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 (
|
|
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 (
|
|
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')])
|
package/dist/context/Context.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
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 (
|
|
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 {
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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 {
|
|
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 =
|
|
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 (
|
|
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__
|
|
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 {
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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;
|
package/dist/utils/batch.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
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 (!
|
|
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 (!
|
|
23
|
+
if (!BROWSER)
|
|
24
24
|
return;
|
|
25
25
|
this.depth += 1;
|
|
26
26
|
}
|
|
27
27
|
end() {
|
|
28
|
-
if (!
|
|
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
|
|
38
|
+
return BROWSER && this.depth > 0;
|
|
39
39
|
}
|
|
40
40
|
queueUpdate(key, value, callback) {
|
|
41
|
-
if (!
|
|
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 (!
|
|
76
|
+
if (!BROWSER) {
|
|
77
77
|
return fn();
|
|
78
78
|
}
|
|
79
79
|
batchManager.begin();
|
package/dist/utils/dev.d.ts
CHANGED
|
@@ -1,9 +1,75 @@
|
|
|
1
|
-
import type
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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 =
|
|
123
|
+
const size = safeJsonLength(value);
|
|
22
124
|
sizeCache.set(value, size);
|
|
23
125
|
return size;
|
|
24
126
|
}
|
|
25
|
-
return
|
|
127
|
+
return safeJsonLength(value);
|
|
26
128
|
},
|
|
27
129
|
warnOnLargeState(key, value) {
|
|
28
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
83
|
-
|
|
84
|
-
version: '1.
|
|
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
|
+
"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
|
-
"
|
|
60
|
-
"
|
|
63
|
+
"@sveltejs/kit": "2.57.1",
|
|
64
|
+
"svelte": "5.53.5"
|
|
61
65
|
},
|
|
62
66
|
"devDependencies": {
|
|
63
|
-
"@
|
|
64
|
-
"@eslint/
|
|
65
|
-
"@
|
|
66
|
-
"@
|
|
67
|
-
"@sveltejs/
|
|
68
|
-
"@sveltejs/
|
|
69
|
-
"@sveltejs/
|
|
70
|
-
"@
|
|
71
|
-
"@testing-library/
|
|
72
|
-
"@
|
|
73
|
-
"
|
|
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": "^
|
|
92
|
-
"
|
|
93
|
-
"
|
|
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",
|