pulse-js-framework 1.7.30 → 1.7.31
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/cli/index.js +20 -5
- package/package.json +12 -2
- package/runtime/async.js +14 -26
- package/runtime/devtools/diagnostics.js +51 -3
- package/runtime/devtools.js +58 -1
- package/runtime/errors.js +159 -0
- package/runtime/graphql.js +83 -113
- package/runtime/http.js +18 -100
- package/runtime/interceptor-manager.js +242 -0
- package/runtime/router.js +80 -15
- package/runtime/utils.js +121 -5
- package/runtime/websocket.js +62 -73
package/cli/index.js
CHANGED
|
@@ -15,8 +15,14 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
15
15
|
const __dirname = dirname(__filename);
|
|
16
16
|
|
|
17
17
|
// Version - read dynamically from package.json
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
let VERSION = '0.0.0';
|
|
19
|
+
try {
|
|
20
|
+
const pkgContent = readFileSync(join(__dirname, '..', 'package.json'), 'utf-8');
|
|
21
|
+
const pkg = JSON.parse(pkgContent);
|
|
22
|
+
VERSION = pkg.version || VERSION;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
log.warn(`Could not read package.json: ${err.message}`);
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
// Available example templates
|
|
22
28
|
const TEMPLATES = {
|
|
@@ -683,10 +689,19 @@ async function initProject(args) {
|
|
|
683
689
|
|
|
684
690
|
if (existsSync(pkgPath)) {
|
|
685
691
|
try {
|
|
686
|
-
|
|
687
|
-
|
|
692
|
+
const pkgContent = readFileSync(pkgPath, 'utf-8');
|
|
693
|
+
if (!pkgContent.trim()) {
|
|
694
|
+
log.warn('Existing package.json is empty, creating new one.');
|
|
695
|
+
} else {
|
|
696
|
+
pkg = JSON.parse(pkgContent);
|
|
697
|
+
log.info('Found existing package.json, merging...');
|
|
698
|
+
}
|
|
688
699
|
} catch (e) {
|
|
689
|
-
|
|
700
|
+
if (e instanceof SyntaxError) {
|
|
701
|
+
log.warn(`Invalid JSON in package.json: ${e.message}. Creating new one.`);
|
|
702
|
+
} else {
|
|
703
|
+
log.warn(`Could not read package.json: ${e.message}. Creating new one.`);
|
|
704
|
+
}
|
|
690
705
|
}
|
|
691
706
|
}
|
|
692
707
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.31",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -109,14 +109,19 @@
|
|
|
109
109
|
"LICENSE"
|
|
110
110
|
],
|
|
111
111
|
"scripts": {
|
|
112
|
-
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:css-parsing && npm run test:pulse && npm run test:dom && npm run test:dom-element && npm run test:dom-adapter && npm run test:enhanced-mock-adapter && npm run test:router && npm run test:store && npm run test:context && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:cli && npm run test:cli-ui && npm run test:cli-create && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:docs-nav && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:a11y-enhanced && npm run test:logger && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build && npm run test:integration && npm run test:context-stress && npm run test:form-edge-cases && npm run test:graphql-subscriptions && npm run test:http-edge-cases && npm run test:integration-advanced && npm run test:websocket-stress && npm run test:ssr",
|
|
112
|
+
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:css-parsing && npm run test:pulse && npm run test:dom && npm run test:dom-element && npm run test:dom-list && npm run test:dom-conditional && npm run test:dom-lifecycle && npm run test:dom-selector && npm run test:dom-adapter && npm run test:dom-advanced && npm run test:enhanced-mock-adapter && npm run test:router && npm run test:store && npm run test:context && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:cli && npm run test:cli-ui && npm run test:cli-create && npm run test:lru-cache && npm run test:utils && npm run test:utils-coverage && npm run test:docs && npm run test:docs-nav && npm run test:async && npm run test:async-coverage && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:a11y-enhanced && npm run test:logger && npm run test:logger-prod && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:graphql-coverage && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build && npm run test:integration && npm run test:context-stress && npm run test:form-edge-cases && npm run test:graphql-subscriptions && npm run test:http-edge-cases && npm run test:integration-advanced && npm run test:websocket-stress && npm run test:ssr && npm run test:ssr-hydrator",
|
|
113
113
|
"test:compiler": "node test/compiler.test.js",
|
|
114
114
|
"test:sourcemap": "node test/sourcemap.test.js",
|
|
115
115
|
"test:css-parsing": "node test/css-parsing.test.js",
|
|
116
116
|
"test:pulse": "node test/pulse.test.js",
|
|
117
117
|
"test:dom": "node test/dom.test.js",
|
|
118
118
|
"test:dom-element": "node test/dom-element.test.js",
|
|
119
|
+
"test:dom-list": "node test/dom-list.test.js",
|
|
120
|
+
"test:dom-conditional": "node test/dom-conditional.test.js",
|
|
121
|
+
"test:dom-lifecycle": "node test/dom-lifecycle.test.js",
|
|
122
|
+
"test:dom-selector": "node test/dom-selector.test.js",
|
|
119
123
|
"test:dom-adapter": "node test/dom-adapter.test.js",
|
|
124
|
+
"test:dom-advanced": "node test/dom-advanced.test.js",
|
|
120
125
|
"test:enhanced-mock-adapter": "node test/enhanced-mock-adapter.test.js",
|
|
121
126
|
"test:router": "node test/router.test.js",
|
|
122
127
|
"test:store": "node test/store.test.js",
|
|
@@ -130,9 +135,11 @@
|
|
|
130
135
|
"test:cli-create": "node test/cli-create.test.js",
|
|
131
136
|
"test:lru-cache": "node test/lru-cache.test.js",
|
|
132
137
|
"test:utils": "node test/utils.test.js",
|
|
138
|
+
"test:utils-coverage": "node test/utils-coverage.test.js",
|
|
133
139
|
"test:docs": "node test/docs.test.js",
|
|
134
140
|
"test:docs-nav": "node test/docs-navigation.test.js",
|
|
135
141
|
"test:async": "node test/async.test.js",
|
|
142
|
+
"test:async-coverage": "node test/async-coverage.test.js",
|
|
136
143
|
"test:form": "node test/form.test.js",
|
|
137
144
|
"test:http": "node test/http.test.js",
|
|
138
145
|
"test:devtools": "node test/devtools.test.js",
|
|
@@ -140,10 +147,12 @@
|
|
|
140
147
|
"test:a11y": "node test/a11y.test.js",
|
|
141
148
|
"test:a11y-enhanced": "node test/a11y-enhanced.test.js",
|
|
142
149
|
"test:logger": "node test/logger.test.js",
|
|
150
|
+
"test:logger-prod": "node test/logger-prod.test.js",
|
|
143
151
|
"test:errors": "node test/errors.test.js",
|
|
144
152
|
"test:security": "node test/security.test.js",
|
|
145
153
|
"test:websocket": "node test/websocket.test.js",
|
|
146
154
|
"test:graphql": "node test/graphql.test.js",
|
|
155
|
+
"test:graphql-coverage": "node test/graphql-coverage.test.js",
|
|
147
156
|
"test:doctor": "node test/doctor.test.js",
|
|
148
157
|
"test:scaffold": "node test/scaffold.test.js",
|
|
149
158
|
"test:test-runner": "node test/test-runner.test.js",
|
|
@@ -156,6 +165,7 @@
|
|
|
156
165
|
"test:integration-advanced": "node test/integration-advanced.test.js",
|
|
157
166
|
"test:websocket-stress": "node test/websocket-stress.test.js",
|
|
158
167
|
"test:ssr": "node test/ssr.test.js",
|
|
168
|
+
"test:ssr-hydrator": "node test/ssr-hydrator.test.js",
|
|
159
169
|
"build:netlify": "node scripts/build-netlify.js",
|
|
160
170
|
"version": "node scripts/sync-version.js",
|
|
161
171
|
"docs": "node cli/index.js dev docs"
|
package/runtime/async.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { pulse, effect, batch, onCleanup } from './pulse.js';
|
|
10
10
|
import { getSSRAsyncContext, registerAsync, getCachedAsync, hasCachedAsync } from './ssr-async.js';
|
|
11
|
+
import { onWindowFocus, onWindowOnline, onNetworkChange } from './utils.js';
|
|
11
12
|
|
|
12
13
|
// ============================================================================
|
|
13
14
|
// Versioned Async - Centralized Race Condition Handling
|
|
@@ -694,26 +695,18 @@ export function useResource(key, fetcher, options = {}) {
|
|
|
694
695
|
}
|
|
695
696
|
|
|
696
697
|
// Setup window focus listener
|
|
697
|
-
if (refreshOnFocus
|
|
698
|
-
|
|
698
|
+
if (refreshOnFocus) {
|
|
699
|
+
onWindowFocus(() => {
|
|
699
700
|
const cached = getCachedData();
|
|
700
701
|
if (!cached || cached.isStale) {
|
|
701
702
|
fetch();
|
|
702
703
|
}
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
window.addEventListener('focus', handleFocus);
|
|
706
|
-
onCleanup(() => window.removeEventListener('focus', handleFocus));
|
|
704
|
+
}, onCleanup);
|
|
707
705
|
}
|
|
708
706
|
|
|
709
707
|
// Setup online listener
|
|
710
|
-
if (refreshOnReconnect
|
|
711
|
-
|
|
712
|
-
fetch();
|
|
713
|
-
};
|
|
714
|
-
|
|
715
|
-
window.addEventListener('online', handleOnline);
|
|
716
|
-
onCleanup(() => window.removeEventListener('online', handleOnline));
|
|
708
|
+
if (refreshOnReconnect) {
|
|
709
|
+
onWindowOnline(() => fetch(), onCleanup);
|
|
717
710
|
}
|
|
718
711
|
|
|
719
712
|
// Track current key for change detection
|
|
@@ -868,19 +861,14 @@ export function usePolling(asyncFn, options) {
|
|
|
868
861
|
}
|
|
869
862
|
|
|
870
863
|
// Online/offline handling
|
|
871
|
-
if (pauseOnOffline
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
window.addEventListener('online', handleOnline);
|
|
880
|
-
onCleanup(() => {
|
|
881
|
-
window.removeEventListener('offline', handleOffline);
|
|
882
|
-
window.removeEventListener('online', handleOnline);
|
|
883
|
-
});
|
|
864
|
+
if (pauseOnOffline) {
|
|
865
|
+
onNetworkChange({
|
|
866
|
+
onOffline: () => pause(),
|
|
867
|
+
onOnline: () => {
|
|
868
|
+
resume();
|
|
869
|
+
if (isPolling.get()) poll();
|
|
870
|
+
}
|
|
871
|
+
}, onCleanup);
|
|
884
872
|
}
|
|
885
873
|
|
|
886
874
|
// Cleanup on unmount
|
|
@@ -29,7 +29,9 @@ export const config = {
|
|
|
29
29
|
logUpdates: false,
|
|
30
30
|
logEffects: false,
|
|
31
31
|
warnOnSlowEffects: true,
|
|
32
|
-
slowEffectThreshold: 16 // ms (one frame at 60fps)
|
|
32
|
+
slowEffectThreshold: 16, // ms (one frame at 60fps)
|
|
33
|
+
autoTimeline: false, // Automatically record all pulse changes to timeline
|
|
34
|
+
timelineDebounce: 100 // Debounce interval for timeline recording (ms)
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
// =============================================================================
|
|
@@ -51,6 +53,46 @@ export const effectRegistry = new Map();
|
|
|
51
53
|
let pulseIdCounter = 0;
|
|
52
54
|
let trackedEffectIdCounter = 0;
|
|
53
55
|
|
|
56
|
+
// Timeline auto-recording state
|
|
57
|
+
let timelineDebounceTimer = null;
|
|
58
|
+
let pendingTimelineActions = [];
|
|
59
|
+
let takeSnapshotFn = null;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set the snapshot function for auto-timeline
|
|
63
|
+
* Called by devtools.js to wire up time-travel
|
|
64
|
+
* @param {Function} fn - takeSnapshot function
|
|
65
|
+
*/
|
|
66
|
+
export function _setTakeSnapshotFn(fn) {
|
|
67
|
+
takeSnapshotFn = fn;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Schedule a timeline snapshot (debounced)
|
|
72
|
+
* @param {string} action - Description of the action
|
|
73
|
+
*/
|
|
74
|
+
function scheduleTimelineSnapshot(action) {
|
|
75
|
+
if (!config.autoTimeline || !takeSnapshotFn) return;
|
|
76
|
+
|
|
77
|
+
pendingTimelineActions.push(action);
|
|
78
|
+
|
|
79
|
+
if (timelineDebounceTimer) {
|
|
80
|
+
clearTimeout(timelineDebounceTimer);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
timelineDebounceTimer = setTimeout(() => {
|
|
84
|
+
timelineDebounceTimer = null;
|
|
85
|
+
if (pendingTimelineActions.length > 0) {
|
|
86
|
+
// Combine multiple actions into one snapshot
|
|
87
|
+
const combinedAction = pendingTimelineActions.length === 1
|
|
88
|
+
? pendingTimelineActions[0]
|
|
89
|
+
: `${pendingTimelineActions.length} changes: ${pendingTimelineActions.slice(0, 3).join(', ')}${pendingTimelineActions.length > 3 ? '...' : ''}`;
|
|
90
|
+
pendingTimelineActions = [];
|
|
91
|
+
takeSnapshotFn(combinedAction);
|
|
92
|
+
}
|
|
93
|
+
}, config.timelineDebounce);
|
|
94
|
+
}
|
|
95
|
+
|
|
54
96
|
// =============================================================================
|
|
55
97
|
// DIAGNOSTICS API
|
|
56
98
|
// =============================================================================
|
|
@@ -267,9 +309,14 @@ export function trackedPulse(initialValue, name, options = {}) {
|
|
|
267
309
|
if (config.enabled && config.logUpdates) {
|
|
268
310
|
log.info(`${name || id} updated:`, value);
|
|
269
311
|
}
|
|
270
|
-
|
|
312
|
+
// Manual snapshot callback (for non-auto mode)
|
|
313
|
+
if (config.enabled && options.onSnapshot && !config.autoTimeline) {
|
|
271
314
|
options.onSnapshot(`${name || id} = ${JSON.stringify(value)}`);
|
|
272
315
|
}
|
|
316
|
+
// Auto-timeline recording (debounced)
|
|
317
|
+
if (config.enabled && config.autoTimeline) {
|
|
318
|
+
scheduleTimelineSnapshot(`${name || id} = ${JSON.stringify(value)}`);
|
|
319
|
+
}
|
|
273
320
|
return result;
|
|
274
321
|
};
|
|
275
322
|
|
|
@@ -399,5 +446,6 @@ export default {
|
|
|
399
446
|
mark,
|
|
400
447
|
resetDiagnostics,
|
|
401
448
|
pulseRegistry,
|
|
402
|
-
effectRegistry
|
|
449
|
+
effectRegistry,
|
|
450
|
+
_setTakeSnapshotFn
|
|
403
451
|
};
|
package/runtime/devtools.js
CHANGED
|
@@ -31,7 +31,8 @@ import {
|
|
|
31
31
|
profile,
|
|
32
32
|
mark,
|
|
33
33
|
resetDiagnostics,
|
|
34
|
-
_setSnapshotCountFn
|
|
34
|
+
_setSnapshotCountFn,
|
|
35
|
+
_setTakeSnapshotFn
|
|
35
36
|
} from './devtools/diagnostics.js';
|
|
36
37
|
|
|
37
38
|
import {
|
|
@@ -50,6 +51,9 @@ import {
|
|
|
50
51
|
// Wire up snapshot count for diagnostics
|
|
51
52
|
_setSnapshotCountFn(getSnapshotCount);
|
|
52
53
|
|
|
54
|
+
// Wire up takeSnapshot for auto-timeline
|
|
55
|
+
_setTakeSnapshotFn(takeSnapshot);
|
|
56
|
+
|
|
53
57
|
import {
|
|
54
58
|
a11yAuditConfig,
|
|
55
59
|
runA11yAudit,
|
|
@@ -88,9 +92,41 @@ export function trackedPulse(initialValue, name) {
|
|
|
88
92
|
// DEV TOOLS API
|
|
89
93
|
// =============================================================================
|
|
90
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Enable automatic timeline recording
|
|
97
|
+
* Records all pulse changes to the timeline automatically (debounced)
|
|
98
|
+
* @param {Object} [options] - Configuration options
|
|
99
|
+
* @param {number} [options.debounce=100] - Debounce interval in ms
|
|
100
|
+
*/
|
|
101
|
+
export function enableAutoTimeline(options = {}) {
|
|
102
|
+
config.autoTimeline = true;
|
|
103
|
+
if (options.debounce !== undefined) {
|
|
104
|
+
config.timelineDebounce = options.debounce;
|
|
105
|
+
}
|
|
106
|
+
log.info(`Auto-timeline enabled (debounce: ${config.timelineDebounce}ms)`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Disable automatic timeline recording
|
|
111
|
+
*/
|
|
112
|
+
export function disableAutoTimeline() {
|
|
113
|
+
config.autoTimeline = false;
|
|
114
|
+
log.info('Auto-timeline disabled');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if auto-timeline is enabled
|
|
119
|
+
* @returns {boolean}
|
|
120
|
+
*/
|
|
121
|
+
export function isAutoTimelineEnabled() {
|
|
122
|
+
return config.autoTimeline;
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
/**
|
|
92
126
|
* Enable dev tools
|
|
93
127
|
* @param {Object} [options] - Configuration options
|
|
128
|
+
* @param {boolean} [options.autoTimeline] - Enable automatic timeline recording
|
|
129
|
+
* @param {number} [options.timelineDebounce] - Debounce interval for timeline (ms)
|
|
94
130
|
*/
|
|
95
131
|
export function enableDevTools(options = {}) {
|
|
96
132
|
Object.assign(config, options, { enabled: true });
|
|
@@ -99,6 +135,14 @@ export function enableDevTools(options = {}) {
|
|
|
99
135
|
timeTravelConfig.maxSnapshots = options.maxSnapshots;
|
|
100
136
|
}
|
|
101
137
|
|
|
138
|
+
// Enable auto-timeline if specified
|
|
139
|
+
if (options.autoTimeline) {
|
|
140
|
+
config.autoTimeline = true;
|
|
141
|
+
if (options.timelineDebounce !== undefined) {
|
|
142
|
+
config.timelineDebounce = options.timelineDebounce;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
102
146
|
if (typeof window !== 'undefined') {
|
|
103
147
|
// Expose to window for browser dev tools
|
|
104
148
|
window.__PULSE_DEVTOOLS__ = {
|
|
@@ -119,6 +163,11 @@ export function enableDevTools(options = {}) {
|
|
|
119
163
|
forward,
|
|
120
164
|
clearHistory,
|
|
121
165
|
|
|
166
|
+
// Auto-timeline
|
|
167
|
+
enableAutoTimeline,
|
|
168
|
+
disableAutoTimeline,
|
|
169
|
+
isAutoTimelineEnabled,
|
|
170
|
+
|
|
122
171
|
// A11y Audit
|
|
123
172
|
runA11yAudit,
|
|
124
173
|
getA11yIssues,
|
|
@@ -216,6 +265,9 @@ export {
|
|
|
216
265
|
resetA11yAudit
|
|
217
266
|
};
|
|
218
267
|
|
|
268
|
+
// Note: enableAutoTimeline, disableAutoTimeline, isAutoTimelineEnabled
|
|
269
|
+
// are already exported via their function declarations above
|
|
270
|
+
|
|
219
271
|
// =============================================================================
|
|
220
272
|
// DEFAULT EXPORT
|
|
221
273
|
// =============================================================================
|
|
@@ -241,6 +293,11 @@ export default {
|
|
|
241
293
|
forward,
|
|
242
294
|
clearHistory,
|
|
243
295
|
|
|
296
|
+
// Auto-timeline
|
|
297
|
+
enableAutoTimeline,
|
|
298
|
+
disableAutoTimeline,
|
|
299
|
+
isAutoTimelineEnabled,
|
|
300
|
+
|
|
244
301
|
// Configuration
|
|
245
302
|
enableDevTools,
|
|
246
303
|
disableDevTools,
|
package/runtime/errors.js
CHANGED
|
@@ -217,6 +217,163 @@ export class RouterError extends RuntimeError {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Client Errors (HTTP, WebSocket, GraphQL)
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Base class for client errors (HTTP, WebSocket, GraphQL).
|
|
226
|
+
* Provides common patterns for error creation with suggestions.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* class HttpError extends ClientError {
|
|
230
|
+
* static suggestions = { TIMEOUT: 'Increase timeout...' };
|
|
231
|
+
* static errorName = 'HttpError';
|
|
232
|
+
* static defaultCode = 'HTTP_ERROR';
|
|
233
|
+
* static markerProperty = 'isHttpError';
|
|
234
|
+
* }
|
|
235
|
+
*/
|
|
236
|
+
export class ClientError extends RuntimeError {
|
|
237
|
+
/** @type {Object<string, string>} Error code to suggestion mapping */
|
|
238
|
+
static suggestions = {};
|
|
239
|
+
|
|
240
|
+
/** @type {string} The error class name */
|
|
241
|
+
static errorName = 'ClientError';
|
|
242
|
+
|
|
243
|
+
/** @type {string} Default error code when none provided */
|
|
244
|
+
static defaultCode = 'CLIENT_ERROR';
|
|
245
|
+
|
|
246
|
+
/** @type {string} Property name for instance type checking (e.g., 'isHttpError') */
|
|
247
|
+
static markerProperty = 'isClientError';
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {string} message - Error message
|
|
251
|
+
* @param {Object} [options={}] - Error options
|
|
252
|
+
* @param {string} [options.code] - Error code
|
|
253
|
+
* @param {string} [options.context] - Error context
|
|
254
|
+
* @param {string} [options.suggestion] - Custom suggestion (overrides default)
|
|
255
|
+
*/
|
|
256
|
+
constructor(message, options = {}) {
|
|
257
|
+
const code = options.code || new.target.defaultCode;
|
|
258
|
+
const suggestion = options.suggestion || new.target.suggestions[code];
|
|
259
|
+
|
|
260
|
+
const formattedMessage = createErrorMessage({
|
|
261
|
+
code,
|
|
262
|
+
message,
|
|
263
|
+
context: options.context,
|
|
264
|
+
suggestion
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
super(formattedMessage, { code });
|
|
268
|
+
|
|
269
|
+
this.name = new.target.errorName;
|
|
270
|
+
this.code = code;
|
|
271
|
+
this[new.target.markerProperty] = true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if an error is an instance of this error type
|
|
276
|
+
* @param {any} error - The error to check
|
|
277
|
+
* @returns {boolean}
|
|
278
|
+
*/
|
|
279
|
+
static isError(error) {
|
|
280
|
+
return error?.[this.markerProperty] === true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check if this is a timeout error
|
|
285
|
+
* @returns {boolean}
|
|
286
|
+
*/
|
|
287
|
+
isTimeout() {
|
|
288
|
+
return this.code === 'TIMEOUT';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Check if this is a network error
|
|
293
|
+
* @returns {boolean}
|
|
294
|
+
*/
|
|
295
|
+
isNetworkError() {
|
|
296
|
+
return this.code === 'NETWORK' || this.code === 'NETWORK_ERROR';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Factory to create a client error class with specific suggestions and properties.
|
|
302
|
+
*
|
|
303
|
+
* @param {Object} config - Error class configuration
|
|
304
|
+
* @param {string} config.name - Error class name (e.g., 'HttpError')
|
|
305
|
+
* @param {string} config.defaultCode - Default error code
|
|
306
|
+
* @param {string} config.markerProperty - Instance marker property (e.g., 'isHttpError')
|
|
307
|
+
* @param {Object<string, string>} config.suggestions - Error code to suggestion mapping
|
|
308
|
+
* @param {string[]} [config.codeMethods] - Error codes to create isXxx() methods for
|
|
309
|
+
* @param {string[]} [config.additionalProperties] - Extra properties to copy from options
|
|
310
|
+
* @returns {typeof ClientError} The created error class
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* const HttpError = createClientErrorClass({
|
|
314
|
+
* name: 'HttpError',
|
|
315
|
+
* defaultCode: 'HTTP_ERROR',
|
|
316
|
+
* markerProperty: 'isHttpError',
|
|
317
|
+
* suggestions: {
|
|
318
|
+
* TIMEOUT: 'Consider increasing the timeout.',
|
|
319
|
+
* NETWORK: 'Check internet connectivity.'
|
|
320
|
+
* },
|
|
321
|
+
* codeMethods: ['TIMEOUT', 'NETWORK', 'ABORT'],
|
|
322
|
+
* additionalProperties: ['config', 'request', 'response', 'status']
|
|
323
|
+
* });
|
|
324
|
+
*/
|
|
325
|
+
export function createClientErrorClass(config) {
|
|
326
|
+
const {
|
|
327
|
+
name,
|
|
328
|
+
defaultCode,
|
|
329
|
+
markerProperty,
|
|
330
|
+
suggestions = {},
|
|
331
|
+
codeMethods = [],
|
|
332
|
+
additionalProperties = []
|
|
333
|
+
} = config;
|
|
334
|
+
|
|
335
|
+
class CustomClientError extends ClientError {
|
|
336
|
+
static suggestions = suggestions;
|
|
337
|
+
static errorName = name;
|
|
338
|
+
static defaultCode = defaultCode;
|
|
339
|
+
static markerProperty = markerProperty;
|
|
340
|
+
|
|
341
|
+
constructor(message, options = {}) {
|
|
342
|
+
super(message, options);
|
|
343
|
+
|
|
344
|
+
// Copy additional properties from options
|
|
345
|
+
for (const prop of additionalProperties) {
|
|
346
|
+
this[prop] = options[prop] ?? null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Static type check method (e.g., HttpError.isHttpError(err))
|
|
352
|
+
*/
|
|
353
|
+
static [`is${name}`](error) {
|
|
354
|
+
return error?.[markerProperty] === true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Add isXxx() methods for each code
|
|
359
|
+
for (const code of codeMethods) {
|
|
360
|
+
const methodName = 'is' + code.split('_').map(
|
|
361
|
+
(part, i) => i === 0
|
|
362
|
+
? part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
363
|
+
: part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
364
|
+
).join('');
|
|
365
|
+
|
|
366
|
+
CustomClientError.prototype[methodName] = function () {
|
|
367
|
+
return this.code === code;
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Set the class name for better debugging
|
|
372
|
+
Object.defineProperty(CustomClientError, 'name', { value: name });
|
|
373
|
+
|
|
374
|
+
return CustomClientError;
|
|
375
|
+
}
|
|
376
|
+
|
|
220
377
|
// ============================================================================
|
|
221
378
|
// CLI Errors
|
|
222
379
|
// ============================================================================
|
|
@@ -564,6 +721,8 @@ export default {
|
|
|
564
721
|
DOMError,
|
|
565
722
|
StoreError,
|
|
566
723
|
RouterError,
|
|
724
|
+
ClientError,
|
|
725
|
+
createClientErrorClass,
|
|
567
726
|
CLIError,
|
|
568
727
|
ConfigError,
|
|
569
728
|
SUGGESTIONS,
|