visualfries 0.1.102 → 0.1.103
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/dist/SceneBuilder.svelte.js +19 -0
- package/dist/builders/_ComponentState.svelte.js +4 -2
- package/dist/commands/SeekCommand.js +20 -0
- package/dist/components/ComponentContext.svelte.js +16 -1
- package/dist/components/ComponentContextHelpers.d.ts +115 -0
- package/dist/components/ComponentContextHelpers.js +196 -0
- package/dist/components/SafeHookRunner.d.ts +52 -0
- package/dist/components/SafeHookRunner.js +67 -0
- package/dist/schemas/runtime/types.d.ts +7 -0
- package/package.json +1 -1
|
@@ -276,6 +276,25 @@ export class SceneBuilder {
|
|
|
276
276
|
},
|
|
277
277
|
checksum: 'new-' + uuidv4()
|
|
278
278
|
};
|
|
279
|
+
// For VIDEO components, adjust source.startAt for the second part
|
|
280
|
+
// source.startAt determines at what point in the video file playback starts
|
|
281
|
+
// when the timeline reaches timeline.startAt
|
|
282
|
+
if (cloneData.type === 'VIDEO') {
|
|
283
|
+
const videoClone = cloneData;
|
|
284
|
+
// Calculate the time offset from original component start to split point
|
|
285
|
+
const timeOffsetFromStart = currentTime - compStart;
|
|
286
|
+
// Get current source.startAt (or 0 if not set/undefined)
|
|
287
|
+
const currentSourceStartAt = videoClone.source?.startAt ?? 0;
|
|
288
|
+
// The new source.startAt is the original offset plus the split offset
|
|
289
|
+
const newSourceStartAt = currentSourceStartAt + timeOffsetFromStart;
|
|
290
|
+
// Ensure source object exists and update startAt
|
|
291
|
+
if (!videoClone.source) {
|
|
292
|
+
videoClone.source = { startAt: newSourceStartAt };
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
videoClone.source.startAt = newSourceStartAt;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
279
298
|
// Update original component's end time
|
|
280
299
|
component.props.setEnd(this.currentTime);
|
|
281
300
|
// Add the cloned component to the same layer
|
|
@@ -179,8 +179,10 @@ export class ComponentState {
|
|
|
179
179
|
// }
|
|
180
180
|
}
|
|
181
181
|
async updateAppearance(appearance) {
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
// Use $state.snapshot() to properly extract all properties from the reactive proxy
|
|
183
|
+
const currentData = $state.snapshot(this.#data);
|
|
184
|
+
const mergedAppearance = merge({}, currentData.appearance, appearance);
|
|
185
|
+
this.#data = { ...currentData, appearance: mergedAppearance };
|
|
184
186
|
this.#emitChange();
|
|
185
187
|
await this.maybeAutoRefresh();
|
|
186
188
|
}
|
|
@@ -23,6 +23,21 @@ export class SeekCommand {
|
|
|
23
23
|
this.timeline.seek(time);
|
|
24
24
|
// Ensure a deterministic render on server after seek to advance media frames
|
|
25
25
|
if (this.state.environment === 'server') {
|
|
26
|
+
// Wait for fonts to be ready before rendering
|
|
27
|
+
// This is critical for subtitle animations that use SplitText -
|
|
28
|
+
// if fonts aren't loaded, text measurements will be wrong and
|
|
29
|
+
// animations may fail silently or not appear on first subtitles
|
|
30
|
+
if (typeof document !== 'undefined' && document.fonts?.ready) {
|
|
31
|
+
try {
|
|
32
|
+
await Promise.race([
|
|
33
|
+
document.fonts.ready,
|
|
34
|
+
new Promise((resolve) => setTimeout(resolve, 2000)) // 2s timeout
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Ignore font loading errors, continue with rendering
|
|
39
|
+
}
|
|
40
|
+
}
|
|
26
41
|
// Try multiple render passes until loading state clears or attempts exhausted
|
|
27
42
|
const maxAttempts = 10;
|
|
28
43
|
for (let i = 0; i < maxAttempts; i += 1) {
|
|
@@ -34,6 +49,11 @@ export class SeekCommand {
|
|
|
34
49
|
if (this.state.state === 'loading') {
|
|
35
50
|
console.warn('SeekCommand: Max render attempts exhausted while still loading');
|
|
36
51
|
}
|
|
52
|
+
// Re-seek to apply correct animation state to any animations
|
|
53
|
+
// that were added during the render passes above.
|
|
54
|
+
// This fixes the race condition where subtitle animations are added
|
|
55
|
+
// AFTER the initial seek, causing them to miss their initial state.
|
|
56
|
+
this.timeline.seek(time);
|
|
37
57
|
}
|
|
38
58
|
}
|
|
39
59
|
}
|
|
@@ -98,7 +98,22 @@ export class ComponentContext {
|
|
|
98
98
|
const sortedHooks = [...hooks].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
99
99
|
for (let i = 0; i < sortedHooks.length; i += 1) {
|
|
100
100
|
const handler = sortedHooks[i];
|
|
101
|
-
|
|
101
|
+
try {
|
|
102
|
+
await handler.handle(type, this);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// Log the error but continue to next hook
|
|
106
|
+
const hookName = handler.constructor?.name ?? `Hook[${i}]`;
|
|
107
|
+
console.warn(`[ComponentContext] Hook "${hookName}" failed during "${type}" for component "${this.id}":`, error instanceof Error ? error.message : String(error));
|
|
108
|
+
// Emit error event for debugging/monitoring
|
|
109
|
+
this.eventManager.emit('hookerror', {
|
|
110
|
+
hookName,
|
|
111
|
+
hookType: type,
|
|
112
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
113
|
+
componentId: this.id,
|
|
114
|
+
timestamp: Date.now()
|
|
115
|
+
});
|
|
116
|
+
}
|
|
102
117
|
}
|
|
103
118
|
}
|
|
104
119
|
destroy() { }
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Context Helpers
|
|
3
|
+
*
|
|
4
|
+
* Type guards and helper functions for type-safe access to component-specific data
|
|
5
|
+
* in hook contexts. Eliminates the need for `as any` casts throughout the codebase.
|
|
6
|
+
*/
|
|
7
|
+
import type { Component, VideoComponent, AudioComponent, ImageComponent, GifComponent, TextComponent, SubtitleComponent, ShapeComponent, ColorComponent, GradientComponent, ComponentSource, IComponentContext } from '..';
|
|
8
|
+
export declare const MEDIA_COMPONENT_TYPES: readonly ["VIDEO", "AUDIO"];
|
|
9
|
+
export declare const SOURCE_COMPONENT_TYPES: readonly ["VIDEO", "AUDIO", "IMAGE", "GIF"];
|
|
10
|
+
export declare const VISUAL_COMPONENT_TYPES: readonly ["VIDEO", "IMAGE", "GIF", "TEXT", "SHAPE", "SUBTITLES", "COLOR", "GRADIENT"];
|
|
11
|
+
export type MediaComponentType = (typeof MEDIA_COMPONENT_TYPES)[number];
|
|
12
|
+
export type SourceComponentType = (typeof SOURCE_COMPONENT_TYPES)[number];
|
|
13
|
+
/**
|
|
14
|
+
* Type guard for VIDEO component
|
|
15
|
+
*/
|
|
16
|
+
export declare function isVideoComponent(data: Component | undefined): data is VideoComponent;
|
|
17
|
+
/**
|
|
18
|
+
* Type guard for AUDIO component
|
|
19
|
+
*/
|
|
20
|
+
export declare function isAudioComponent(data: Component | undefined): data is AudioComponent;
|
|
21
|
+
/**
|
|
22
|
+
* Type guard for IMAGE component
|
|
23
|
+
*/
|
|
24
|
+
export declare function isImageComponent(data: Component | undefined): data is ImageComponent;
|
|
25
|
+
/**
|
|
26
|
+
* Type guard for GIF component
|
|
27
|
+
*/
|
|
28
|
+
export declare function isGifComponent(data: Component | undefined): data is GifComponent;
|
|
29
|
+
/**
|
|
30
|
+
* Type guard for TEXT component
|
|
31
|
+
*/
|
|
32
|
+
export declare function isTextComponent(data: Component | undefined): data is TextComponent;
|
|
33
|
+
/**
|
|
34
|
+
* Type guard for SUBTITLES component
|
|
35
|
+
*/
|
|
36
|
+
export declare function isSubtitleComponent(data: Component | undefined): data is SubtitleComponent;
|
|
37
|
+
/**
|
|
38
|
+
* Type guard for SHAPE component
|
|
39
|
+
*/
|
|
40
|
+
export declare function isShapeComponent(data: Component | undefined): data is ShapeComponent;
|
|
41
|
+
/**
|
|
42
|
+
* Type guard for COLOR component
|
|
43
|
+
*/
|
|
44
|
+
export declare function isColorComponent(data: Component | undefined): data is ColorComponent;
|
|
45
|
+
/**
|
|
46
|
+
* Type guard for GRADIENT component
|
|
47
|
+
*/
|
|
48
|
+
export declare function isGradientComponent(data: Component | undefined): data is GradientComponent;
|
|
49
|
+
/**
|
|
50
|
+
* Type guard for components with a media source (VIDEO, AUDIO, IMAGE, GIF)
|
|
51
|
+
*/
|
|
52
|
+
export declare function hasSource(data: Component | undefined): data is VideoComponent | AudioComponent | ImageComponent | GifComponent;
|
|
53
|
+
/**
|
|
54
|
+
* Type guard for media components (VIDEO, AUDIO)
|
|
55
|
+
*/
|
|
56
|
+
export declare function isMediaComponent(data: Component | undefined): data is VideoComponent | AudioComponent;
|
|
57
|
+
/**
|
|
58
|
+
* Safely get source from component data
|
|
59
|
+
*/
|
|
60
|
+
export declare function getSource(data: Component | undefined): ComponentSource | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Safely get source.url from component data
|
|
63
|
+
*/
|
|
64
|
+
export declare function getSourceUrl(data: Component | undefined): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Safely get source.startAt from component data (for VIDEO/AUDIO)
|
|
67
|
+
*/
|
|
68
|
+
export declare function getSourceStartAt(data: Component | undefined): number | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Safely get muted state from media component
|
|
71
|
+
*/
|
|
72
|
+
export declare function getMuted(data: Component | undefined): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Safely get volume from media component
|
|
75
|
+
*/
|
|
76
|
+
export declare function getVolume(data: Component | undefined): number;
|
|
77
|
+
/**
|
|
78
|
+
* Safely get text content from TEXT component
|
|
79
|
+
*/
|
|
80
|
+
export declare function getTextContent(data: Component | undefined): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Get typed component data from context
|
|
83
|
+
*/
|
|
84
|
+
export declare function getContextData<T extends Component>(context: IComponentContext, typeGuard: (data: Component | undefined) => data is T): T | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Get video component data from context
|
|
87
|
+
*/
|
|
88
|
+
export declare function getVideoData(context: IComponentContext): VideoComponent | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Get audio component data from context
|
|
91
|
+
*/
|
|
92
|
+
export declare function getAudioData(context: IComponentContext): AudioComponent | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Get image component data from context
|
|
95
|
+
*/
|
|
96
|
+
export declare function getImageData(context: IComponentContext): ImageComponent | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Get gif component data from context
|
|
99
|
+
*/
|
|
100
|
+
export declare function getGifData(context: IComponentContext): GifComponent | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Get text component data from context
|
|
103
|
+
*/
|
|
104
|
+
export declare function getTextData(context: IComponentContext): TextComponent | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Get source from context's component data
|
|
107
|
+
*/
|
|
108
|
+
export declare function getContextSource(context: IComponentContext): ComponentSource | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Get media properties (muted, volume) from context
|
|
111
|
+
*/
|
|
112
|
+
export declare function getMediaProps(context: IComponentContext): {
|
|
113
|
+
muted: boolean;
|
|
114
|
+
volume: number;
|
|
115
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Context Helpers
|
|
3
|
+
*
|
|
4
|
+
* Type guards and helper functions for type-safe access to component-specific data
|
|
5
|
+
* in hook contexts. Eliminates the need for `as any` casts throughout the codebase.
|
|
6
|
+
*/
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Component Type Constants
|
|
9
|
+
// ============================================================================
|
|
10
|
+
export const MEDIA_COMPONENT_TYPES = ['VIDEO', 'AUDIO'];
|
|
11
|
+
export const SOURCE_COMPONENT_TYPES = ['VIDEO', 'AUDIO', 'IMAGE', 'GIF'];
|
|
12
|
+
export const VISUAL_COMPONENT_TYPES = ['VIDEO', 'IMAGE', 'GIF', 'TEXT', 'SHAPE', 'SUBTITLES', 'COLOR', 'GRADIENT'];
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Type Guards for Component Data
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Type guard for VIDEO component
|
|
18
|
+
*/
|
|
19
|
+
export function isVideoComponent(data) {
|
|
20
|
+
return data?.type === 'VIDEO';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Type guard for AUDIO component
|
|
24
|
+
*/
|
|
25
|
+
export function isAudioComponent(data) {
|
|
26
|
+
return data?.type === 'AUDIO';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Type guard for IMAGE component
|
|
30
|
+
*/
|
|
31
|
+
export function isImageComponent(data) {
|
|
32
|
+
return data?.type === 'IMAGE';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Type guard for GIF component
|
|
36
|
+
*/
|
|
37
|
+
export function isGifComponent(data) {
|
|
38
|
+
return data?.type === 'GIF';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Type guard for TEXT component
|
|
42
|
+
*/
|
|
43
|
+
export function isTextComponent(data) {
|
|
44
|
+
return data?.type === 'TEXT';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Type guard for SUBTITLES component
|
|
48
|
+
*/
|
|
49
|
+
export function isSubtitleComponent(data) {
|
|
50
|
+
return data?.type === 'SUBTITLES';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Type guard for SHAPE component
|
|
54
|
+
*/
|
|
55
|
+
export function isShapeComponent(data) {
|
|
56
|
+
return data?.type === 'SHAPE';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Type guard for COLOR component
|
|
60
|
+
*/
|
|
61
|
+
export function isColorComponent(data) {
|
|
62
|
+
return data?.type === 'COLOR';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Type guard for GRADIENT component
|
|
66
|
+
*/
|
|
67
|
+
export function isGradientComponent(data) {
|
|
68
|
+
return data?.type === 'GRADIENT';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Type guard for components with a media source (VIDEO, AUDIO, IMAGE, GIF)
|
|
72
|
+
*/
|
|
73
|
+
export function hasSource(data) {
|
|
74
|
+
return (data !== undefined &&
|
|
75
|
+
SOURCE_COMPONENT_TYPES.includes(data.type));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Type guard for media components (VIDEO, AUDIO)
|
|
79
|
+
*/
|
|
80
|
+
export function isMediaComponent(data) {
|
|
81
|
+
return (data !== undefined &&
|
|
82
|
+
MEDIA_COMPONENT_TYPES.includes(data.type));
|
|
83
|
+
}
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Safe Accessor Helpers
|
|
86
|
+
// ============================================================================
|
|
87
|
+
/**
|
|
88
|
+
* Safely get source from component data
|
|
89
|
+
*/
|
|
90
|
+
export function getSource(data) {
|
|
91
|
+
if (hasSource(data)) {
|
|
92
|
+
return data.source;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Safely get source.url from component data
|
|
98
|
+
*/
|
|
99
|
+
export function getSourceUrl(data) {
|
|
100
|
+
return getSource(data)?.url;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Safely get source.startAt from component data (for VIDEO/AUDIO)
|
|
104
|
+
*/
|
|
105
|
+
export function getSourceStartAt(data) {
|
|
106
|
+
const startAt = getSource(data)?.startAt;
|
|
107
|
+
// Convert null to undefined for consistency
|
|
108
|
+
return startAt ?? undefined;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Safely get muted state from media component
|
|
112
|
+
*/
|
|
113
|
+
export function getMuted(data) {
|
|
114
|
+
if (isVideoComponent(data) || isAudioComponent(data)) {
|
|
115
|
+
return data.muted ?? false;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Safely get volume from media component
|
|
121
|
+
*/
|
|
122
|
+
export function getVolume(data) {
|
|
123
|
+
if (isVideoComponent(data) || isAudioComponent(data)) {
|
|
124
|
+
return data.volume ?? 1;
|
|
125
|
+
}
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Safely get text content from TEXT component
|
|
130
|
+
*/
|
|
131
|
+
export function getTextContent(data) {
|
|
132
|
+
if (isTextComponent(data)) {
|
|
133
|
+
return data.text;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Context Helper Functions
|
|
139
|
+
// ============================================================================
|
|
140
|
+
/**
|
|
141
|
+
* Get typed component data from context
|
|
142
|
+
*/
|
|
143
|
+
export function getContextData(context, typeGuard) {
|
|
144
|
+
const data = context.contextData;
|
|
145
|
+
if (typeGuard(data)) {
|
|
146
|
+
return data;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get video component data from context
|
|
152
|
+
*/
|
|
153
|
+
export function getVideoData(context) {
|
|
154
|
+
return getContextData(context, isVideoComponent);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get audio component data from context
|
|
158
|
+
*/
|
|
159
|
+
export function getAudioData(context) {
|
|
160
|
+
return getContextData(context, isAudioComponent);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get image component data from context
|
|
164
|
+
*/
|
|
165
|
+
export function getImageData(context) {
|
|
166
|
+
return getContextData(context, isImageComponent);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get gif component data from context
|
|
170
|
+
*/
|
|
171
|
+
export function getGifData(context) {
|
|
172
|
+
return getContextData(context, isGifComponent);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get text component data from context
|
|
176
|
+
*/
|
|
177
|
+
export function getTextData(context) {
|
|
178
|
+
return getContextData(context, isTextComponent);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get source from context's component data
|
|
182
|
+
*/
|
|
183
|
+
export function getContextSource(context) {
|
|
184
|
+
const data = context.contextData;
|
|
185
|
+
return getSource(data);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get media properties (muted, volume) from context
|
|
189
|
+
*/
|
|
190
|
+
export function getMediaProps(context) {
|
|
191
|
+
const data = context.contextData;
|
|
192
|
+
return {
|
|
193
|
+
muted: getMuted(data),
|
|
194
|
+
volume: getVolume(data)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe Hook Runner
|
|
3
|
+
*
|
|
4
|
+
* Utility for safely executing hooks with error boundaries.
|
|
5
|
+
* Prevents one failing hook from crashing the entire component lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
import type { EventManager } from '../managers/EventManager.js';
|
|
8
|
+
export interface HookError {
|
|
9
|
+
hookName: string;
|
|
10
|
+
hookType: string;
|
|
11
|
+
error: Error;
|
|
12
|
+
componentId: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SafeHookRunnerOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Whether to continue executing remaining hooks after one fails
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
20
|
+
continueOnError?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to log errors to console
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
logErrors?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Event manager to emit error events
|
|
28
|
+
*/
|
|
29
|
+
eventManager?: EventManager;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Safely execute a single hook handler with error boundary
|
|
33
|
+
*/
|
|
34
|
+
export declare function safeExecuteHook<T>(hookName: string, hookType: string, componentId: string, handler: () => Promise<T> | T, options?: SafeHookRunnerOptions): Promise<{
|
|
35
|
+
success: boolean;
|
|
36
|
+
result?: T;
|
|
37
|
+
error?: HookError;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Execute multiple hooks in sequence with error boundaries
|
|
41
|
+
*/
|
|
42
|
+
export declare function safeExecuteHooks(hooks: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
handler: () => Promise<void> | void;
|
|
45
|
+
}>, hookType: string, componentId: string, options?: SafeHookRunnerOptions): Promise<{
|
|
46
|
+
allSucceeded: boolean;
|
|
47
|
+
errors: HookError[];
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Create a wrapped version of a hook handler that catches errors
|
|
51
|
+
*/
|
|
52
|
+
export declare function createSafeHandler<T extends (...args: any[]) => Promise<any> | any>(hookName: string, hookType: string, componentId: string, handler: T, options?: SafeHookRunnerOptions): T;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe Hook Runner
|
|
3
|
+
*
|
|
4
|
+
* Utility for safely executing hooks with error boundaries.
|
|
5
|
+
* Prevents one failing hook from crashing the entire component lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
const defaultOptions = {
|
|
8
|
+
continueOnError: true,
|
|
9
|
+
logErrors: true
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Safely execute a single hook handler with error boundary
|
|
13
|
+
*/
|
|
14
|
+
export async function safeExecuteHook(hookName, hookType, componentId, handler, options = {}) {
|
|
15
|
+
const opts = { ...defaultOptions, ...options };
|
|
16
|
+
try {
|
|
17
|
+
const result = await handler();
|
|
18
|
+
return { success: true, result };
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
22
|
+
const hookError = {
|
|
23
|
+
hookName,
|
|
24
|
+
hookType,
|
|
25
|
+
error,
|
|
26
|
+
componentId,
|
|
27
|
+
timestamp: Date.now()
|
|
28
|
+
};
|
|
29
|
+
if (opts.logErrors) {
|
|
30
|
+
console.warn(`[SafeHookRunner] Hook "${hookName}" failed during "${hookType}" for component "${componentId}":`, error.message);
|
|
31
|
+
}
|
|
32
|
+
if (opts.eventManager) {
|
|
33
|
+
opts.eventManager.emit('hookerror', hookError);
|
|
34
|
+
}
|
|
35
|
+
return { success: false, error: hookError };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execute multiple hooks in sequence with error boundaries
|
|
40
|
+
*/
|
|
41
|
+
export async function safeExecuteHooks(hooks, hookType, componentId, options = {}) {
|
|
42
|
+
const opts = { ...defaultOptions, ...options };
|
|
43
|
+
const errors = [];
|
|
44
|
+
for (const hook of hooks) {
|
|
45
|
+
const result = await safeExecuteHook(hook.name, hookType, componentId, hook.handler, options);
|
|
46
|
+
if (!result.success && result.error) {
|
|
47
|
+
errors.push(result.error);
|
|
48
|
+
if (!opts.continueOnError) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
allSucceeded: errors.length === 0,
|
|
55
|
+
errors
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a wrapped version of a hook handler that catches errors
|
|
60
|
+
*/
|
|
61
|
+
export function createSafeHandler(hookName, hookType, componentId, handler, options = {}) {
|
|
62
|
+
const wrapped = async (...args) => {
|
|
63
|
+
const result = await safeExecuteHook(hookName, hookType, componentId, () => handler(...args), options);
|
|
64
|
+
return result.result;
|
|
65
|
+
};
|
|
66
|
+
return wrapped;
|
|
67
|
+
}
|
|
@@ -37,6 +37,13 @@ export interface LayerEvents {
|
|
|
37
37
|
export interface ComponentEvents {
|
|
38
38
|
componentschange: void;
|
|
39
39
|
componentchange: ComponentData;
|
|
40
|
+
hookerror: {
|
|
41
|
+
hookName: string;
|
|
42
|
+
hookType: string;
|
|
43
|
+
error: Error;
|
|
44
|
+
componentId: string;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
};
|
|
40
47
|
}
|
|
41
48
|
export interface SubtitlesEvents {
|
|
42
49
|
subtitleschange: void;
|