From 5846dc24161b042111735bcf062f775e212d791a Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 16 Jul 2026 10:39:03 +0700 Subject: [PATCH] perf(extensions): invoke goja providers directly --- go_backend/exports_extensions.go | 18 +- go_backend/extension_provider_wrapper.go | 418 ++++++++++++----------- go_backend/extension_timeout.go | 25 +- 3 files changed, 237 insertions(+), 224 deletions(-) diff --git a/go_backend/exports_extensions.go b/go_backend/exports_extensions.go index 6af3c95a..861cb09e 100644 --- a/go_backend/exports_extensions.go +++ b/go_backend/exports_extensions.go @@ -1059,22 +1059,10 @@ func callExtensionFunctionJSONWithRequestID(extensionID, functionName string, ti } } - // Goja runtime is not thread-safe; guard direct extension.*() calls with VMMu - // to avoid races with other provider calls (e.g. getAlbum/getPlaylist). - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.%s === 'function') { - return extension.%s(); - } - if (typeof %s === 'function') { - return %s(); - } - return null; - })() - `, functionName, functionName, functionName, functionName) - jsStartedAt := time.Now() - result, err := RunWithTimeoutContextAndRecover(requestCtx, vm, script, timeout) + result, err := runGojaCallWithTimeoutContextAndRecover(requestCtx, vm, func() (goja.Value, error) { + return invokeExtensionOrGlobal(vm, functionName) + }, timeout) perf.recordJS(time.Since(jsStartedAt)) if err != nil { if IsRuntimeUnsafeError(err) { diff --git a/go_backend/extension_provider_wrapper.go b/go_backend/extension_provider_wrapper.go index 09b64e5b..3e7be823 100644 --- a/go_backend/extension_provider_wrapper.go +++ b/go_backend/extension_provider_wrapper.go @@ -2,7 +2,6 @@ package gobackend import ( "context" - "encoding/json" "errors" "fmt" "strings" @@ -32,19 +31,18 @@ func (p *extensionProviderWrapper) lockReadyVM() error { return nil } -// extCallOpts configures a shared extension script invocation. It covers the +// extCallOpts configures a shared extension invocation. It covers the // skeleton common to most extensionProviderWrapper methods: perf tracking, VM -// locking, optional download/request cancellation binding, running the -// script, and translating timeouts/cancellation into the right error. +// locking, optional download/request cancellation binding, and translating +// timeouts/cancellation into the right error. type extCallOpts struct { perfName string - script string + invoke func(vm *goja.Runtime) (goja.Value, error) timeout time.Duration itemID string // optional: binds download-cancel + active-item tracking requestID string // optional: binds request-cancel via context (customSearch only) - // beforeRun runs after lock+cancel setup, right before the script executes - // (used to stash query/options as globals instead of embedding them in the - // script source). Its returned cleanup, if any, runs after the script call. + // beforeRun runs after lock+cancel setup, right before the invocation. Its + // returned cleanup, if any, runs after the call. beforeRun func() func() // timeoutMessage overrides the default " timeout: extension took // too long to respond". @@ -54,11 +52,11 @@ type extCallOpts struct { rawError bool } -// callExtensionScript locks the extension's VM, runs opts.script, and hands +// callExtension locks the extension's VM, runs opts.invoke, and hands // the raw result to parse while the VM lock is still held. parse is where // each caller does its type-specific parsing, perf.recordParse/setItems, and // any ProviderID stamping. -func callExtensionScript[T any](p *extensionProviderWrapper, opts extCallOpts, parse func(perf *extensionCallPerf, result goja.Value) (T, error)) (T, error) { +func callExtension[T any](p *extensionProviderWrapper, opts extCallOpts, parse func(perf *extensionCallPerf, result goja.Value) (T, error)) (T, error) { var zero T perf := newExtensionCallPerf(p.extension.ID, opts.perfName) @@ -102,7 +100,9 @@ func callExtensionScript[T any](p *extensionProviderWrapper, opts extCallOpts, p } jsStartedAt := time.Now() - result, err := RunWithTimeoutContextAndRecover(ctx, p.vm, opts.script, opts.timeout) + result, err := runGojaCallWithTimeoutContextAndRecover(ctx, p.vm, func() (goja.Value, error) { + return opts.invoke(p.vm) + }, opts.timeout) perf.recordJS(time.Since(jsStartedAt)) perf.recordPayload(result) if err != nil { @@ -139,6 +139,131 @@ func callExtensionScript[T any](p *extensionProviderWrapper, opts extCallOpts, p return parse(perf, result) } +func invokeExtensionMethod(vm *goja.Runtime, method string, args ...any) (goja.Value, error) { + extensionValue := vm.Get("extension") + if gojaValueIsEmpty(extensionValue) { + return goja.Null(), nil + } + + extensionObject := extensionValue.ToObject(vm) + callable, ok := goja.AssertFunction(extensionObject.Get(method)) + if !ok { + return goja.Null(), nil + } + + values := make([]goja.Value, len(args)) + for i, arg := range args { + values[i] = gojaArgumentValue(vm, arg) + } + return callable(extensionObject, values...) +} + +func gojaArgumentValue(vm *goja.Runtime, value any) goja.Value { + switch typed := value.(type) { + case goja.Value: + return typed + case map[string]any: + obj := vm.NewObject() + for key, child := range typed { + _ = obj.Set(key, gojaArgumentValue(vm, child)) + } + return obj + case map[string]string: + obj := vm.NewObject() + for key, child := range typed { + _ = obj.Set(key, child) + } + return obj + case []any: + children := make([]any, len(typed)) + for i, child := range typed { + children[i] = gojaArgumentValue(vm, child) + } + return vm.NewArray(children...) + case []string: + children := make([]any, len(typed)) + for i, child := range typed { + children[i] = child + } + return vm.NewArray(children...) + default: + return vm.ToValue(value) + } +} + +func extensionMethodInvocation(method string, args ...any) func(*goja.Runtime) (goja.Value, error) { + return func(vm *goja.Runtime) (goja.Value, error) { + return invokeExtensionMethod(vm, method, args...) + } +} + +func hasExtensionMethod(vm *goja.Runtime, method string) bool { + extensionValue := vm.Get("extension") + if gojaValueIsEmpty(extensionValue) { + return false + } + _, ok := goja.AssertFunction(extensionValue.ToObject(vm).Get(method)) + return ok +} + +func invokeExtensionOrGlobal(vm *goja.Runtime, method string, args ...any) (goja.Value, error) { + if hasExtensionMethod(vm, method) { + return invokeExtensionMethod(vm, method, args...) + } + callable, ok := goja.AssertFunction(vm.Get(method)) + if !ok { + return goja.Null(), nil + } + values := make([]goja.Value, len(args)) + for i, arg := range args { + values[i] = gojaArgumentValue(vm, arg) + } + return callable(vm.GlobalObject(), values...) +} + +func extensionTrackInput(track *ExtTrackMetadata) map[string]any { + if track == nil { + return map[string]any{} + } + return map[string]any{ + "id": track.ID, + "name": track.Name, + "artists": track.Artists, + "album_name": track.AlbumName, + "album_artist": track.AlbumArtist, + "album_id": track.AlbumID, + "album_url": track.AlbumURL, + "artist_id": track.ArtistID, + "artist_url": track.ArtistURL, + "external_urls": track.ExternalURL, + "duration_ms": track.DurationMS, + "cover_url": track.CoverURL, + "preview_url": track.PreviewURL, + "images": track.Images, + "release_date": track.ReleaseDate, + "track_number": track.TrackNumber, + "total_tracks": track.TotalTracks, + "disc_number": track.DiscNumber, + "total_discs": track.TotalDiscs, + "isrc": track.ISRC, + "provider_id": track.ProviderID, + "item_type": track.ItemType, + "album_type": track.AlbumType, + "explicit": track.Explicit, + "tidal_id": track.TidalID, + "qobuz_id": track.QobuzID, + "deezer_id": track.DeezerID, + "spotify_id": track.SpotifyID, + "external_links": track.ExternalLinks, + "label": track.Label, + "copyright": track.Copyright, + "genre": track.Genre, + "composer": track.Composer, + "audio_quality": track.AudioQuality, + "audio_modes": track.AudioModes, + } +} + func (p *extensionProviderWrapper) SearchTracks(query string, limit int) (*ExtSearchResult, error) { return p.SearchTracksForItemID(query, limit, "") } @@ -151,18 +276,9 @@ func (p *extensionProviderWrapper) SearchTracksForItemID(query string, limit int return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.searchTracks === 'function') { - return extension.searchTracks(%q, %d); - } - return null; - })() - `, query, limit) - - return callExtensionScript(p, extCallOpts{ + return callExtension(p, extCallOpts{ perfName: "searchTracks", - script: script, + invoke: extensionMethodInvocation("searchTracks", query, limit), timeout: DefaultJSTimeout, itemID: itemID, }, func(perf *extensionCallPerf, result goja.Value) (*ExtSearchResult, error) { @@ -193,18 +309,9 @@ func (p *extensionProviderWrapper) GetTrack(trackID string) (*ExtTrackMetadata, return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.getTrack === 'function') { - return extension.getTrack(%q); - } - return null; - })() - `, trackID) - - return callExtensionScript(p, extCallOpts{ + return callExtension(p, extCallOpts{ perfName: "getTrack", - script: script, + invoke: extensionMethodInvocation("getTrack", trackID), timeout: DefaultJSTimeout, }, func(perf *extensionCallPerf, result goja.Value) (*ExtTrackMetadata, error) { if result == nil || goja.IsUndefined(result) || goja.IsNull(result) { @@ -227,18 +334,9 @@ func (p *extensionProviderWrapper) GetAlbum(albumID string) (*ExtAlbumMetadata, return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.getAlbum === 'function') { - return extension.getAlbum(%q); - } - return null; - })() - `, albumID) - - return callExtensionScript(p, extCallOpts{ + return callExtension(p, extCallOpts{ perfName: "getAlbum", - script: script, + invoke: extensionMethodInvocation("getAlbum", albumID), timeout: DefaultJSTimeout, }, func(perf *extensionCallPerf, result goja.Value) (*ExtAlbumMetadata, error) { if result == nil || goja.IsUndefined(result) || goja.IsNull(result) { @@ -268,22 +366,15 @@ func (p *extensionProviderWrapper) GetPlaylist(playlistID string) (*ExtAlbumMeta return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.getPlaylist === 'function') { - return extension.getPlaylist(%q); - } - if (typeof extension !== 'undefined' && typeof extension.getAlbum === 'function') { - return extension.getAlbum(%q); - } - return null; - })() - `, playlistID, playlistID) - - return callExtensionScript(p, extCallOpts{ + return callExtension(p, extCallOpts{ perfName: "getPlaylist", - script: script, - timeout: DefaultJSTimeout, + invoke: func(vm *goja.Runtime) (goja.Value, error) { + if hasExtensionMethod(vm, "getPlaylist") { + return invokeExtensionMethod(vm, "getPlaylist", playlistID) + } + return invokeExtensionMethod(vm, "getAlbum", playlistID) + }, + timeout: DefaultJSTimeout, }, func(perf *extensionCallPerf, result goja.Value) (*ExtAlbumMetadata, error) { if result == nil || goja.IsUndefined(result) || goja.IsNull(result) { return nil, fmt.Errorf("getPlaylist returned null") @@ -312,18 +403,9 @@ func (p *extensionProviderWrapper) GetArtist(artistID string) (*ExtArtistMetadat return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.getArtist === 'function') { - return extension.getArtist(%q); - } - return null; - })() - `, artistID) - - return callExtensionScript(p, extCallOpts{ + return callExtension(p, extCallOpts{ perfName: "getArtist", - script: script, + invoke: extensionMethodInvocation("getArtist", artistID), timeout: DefaultJSTimeout, }, func(perf *extensionCallPerf, result goja.Value) (*ExtArtistMetadata, error) { if result == nil || goja.IsUndefined(result) || goja.IsNull(result) { @@ -352,7 +434,7 @@ func (p *extensionProviderWrapper) EnrichTrack(track *ExtTrackMetadata) (*ExtTra return p.EnrichTrackForItemID(track, "") } -// EnrichTrackForItemID is excluded from the shared callExtensionScript helper: +// EnrichTrackForItemID is excluded from the shared callExtension helper: // unlike the other providers it must return the original track (not an error) // on every failure path, which doesn't fit the helper's error-returning shape. func (p *extensionProviderWrapper) EnrichTrackForItemID(track *ExtTrackMetadata, itemID string) (*ExtTrackMetadata, error) { @@ -384,24 +466,10 @@ func (p *extensionProviderWrapper) EnrichTrackForItemID(track *ExtTrackMetadata, } } - trackJSON, err := json.Marshal(track) - if err != nil { - GoLog("[Extension] EnrichTrack: failed to marshal track: %v\n", err) - return track, nil - } - - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.enrichTrack === 'function') { - var track = %s; - return extension.enrichTrack(track); - } - return null; - })() - `, string(trackJSON)) - jsStartedAt := time.Now() - result, err := RunWithTimeoutAndRecover(p.vm, script, DefaultJSTimeout) + result, err := runGojaCallWithTimeoutAndRecover(p.vm, func() (goja.Value, error) { + return invokeExtensionMethod(p.vm, "enrichTrack", extensionTrackInput(track)) + }, DefaultJSTimeout) perf.recordJS(time.Since(jsStartedAt)) perf.recordPayload(result) if err != nil { @@ -443,24 +511,17 @@ func (p *extensionProviderWrapper) CheckAvailabilityForItemID(isrc, trackName, a return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.checkAvailability === 'function') { - return extension.checkAvailability(%q, %q, %q, { - spotify_id: %q, - deezer_id: %q, - tidal_id: %q, - qobuz_id: %q, - duration_ms: %d - }); - } - return null; - })() - `, isrc, trackName, artistName, spotifyID, deezerID, tidalID, qobuzID, durationMS) + availabilityOptions := map[string]any{ + "spotify_id": spotifyID, + "deezer_id": deezerID, + "tidal_id": tidalID, + "qobuz_id": qobuzID, + "duration_ms": durationMS, + } - return callExtensionScript(p, extCallOpts{ + return callExtension(p, extCallOpts{ perfName: "checkAvailability", - script: script, + invoke: extensionMethodInvocation("checkAvailability", isrc, trackName, artistName, availabilityOptions), timeout: DefaultJSTimeout, itemID: itemID, beforeRun: func() func() { @@ -498,7 +559,7 @@ func (p *extensionProviderWrapper) CheckAvailabilityForItemID(isrc, trackName, a const ExtDownloadTimeout = DownloadTimeout -// Download is excluded from the shared callExtensionScript helper: it runs in +// Download is excluded from the shared callExtension helper: it runs in // an isolated VM/runtime (not p.vm/p.extension.VMMu) with a progress // callback, which the helper's lock+perf model doesn't cover. func (p *extensionProviderWrapper) Download(trackID, quality, outputPath, itemID string, onProgress func(percent int)) (*ExtDownloadResult, error) { @@ -536,7 +597,7 @@ func (p *extensionProviderWrapper) Download(trackID, quality, outputPath, itemID SetItemPreparing(itemID) } - vm.Set("__onProgress", func(call goja.FunctionCall) goja.Value { + progressCallback := vm.ToValue(func(call goja.FunctionCall) goja.Value { if len(call.Arguments) > 0 { percent := int(call.Arguments[0].ToInteger()) if percent < 0 { @@ -552,15 +613,6 @@ func (p *extensionProviderWrapper) Download(trackID, quality, outputPath, itemID return goja.Undefined() }) - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.download === 'function') { - return extension.download(%q, %q, %q, __onProgress); - } - return null; - })() - `, trackID, quality, outputPath) - if runtime != nil { // Drop any stale flag (pooled runtimes survive across downloads) so // the post-run check only sees verification from THIS call. @@ -568,7 +620,9 @@ func (p *extensionProviderWrapper) Download(trackID, quality, outputPath, itemID } jsStartedAt := time.Now() - result, err := RunWithTimeoutAndRecover(vm, script, ExtDownloadTimeout) + result, err := runGojaCallWithTimeoutAndRecover(vm, func() (goja.Value, error) { + return invokeExtensionMethod(vm, "download", trackID, quality, outputPath, progressCallback) + }, ExtDownloadTimeout) perf.recordJS(time.Since(jsStartedAt)) perf.recordPayload(result) vmHealthy = err == nil @@ -643,35 +697,12 @@ func (p *extensionProviderWrapper) customSearch(query string, options map[string options = map[string]any{} } - // Avoid embedding user input directly into JS source. Some inputs can trigger - // parser/runtime edge cases on specific devices/Goja builds. - const queryVar = "__sf_custom_search_query" - const optionsVar = "__sf_custom_search_options" - - const script = ` - (function() { - if (typeof extension !== 'undefined' && typeof extension.customSearch === 'function') { - return extension.customSearch(__sf_custom_search_query, __sf_custom_search_options); - } - return null; - })() - ` - - return callExtensionScript(p, extCallOpts{ + return callExtension(p, extCallOpts{ perfName: "customSearch", - script: script, + invoke: extensionMethodInvocation("customSearch", query, options), timeout: DefaultJSTimeout, itemID: itemID, requestID: requestID, - beforeRun: func() func() { - global := p.vm.GlobalObject() - _ = global.Set(queryVar, query) - _ = global.Set(optionsVar, options) - return func() { - global.Delete(queryVar) - global.Delete(optionsVar) - } - }, }, func(perf *extensionCallPerf, result goja.Value) ([]ExtTrackMetadata, error) { if result == nil || goja.IsUndefined(result) || goja.IsNull(result) { return []ExtTrackMetadata{}, nil @@ -712,18 +743,9 @@ func (p *extensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - script := fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.handleUrl === 'function') { - return extension.handleUrl(%q); - } - return null; - })() - `, url) - - return callExtensionScript(p, extCallOpts{ + return callExtension(p, extCallOpts{ perfName: "handleUrl", - script: script, + invoke: extensionMethodInvocation("handleUrl", url), timeout: DefaultJSTimeout, }, func(perf *extensionCallPerf, result goja.Value) (*ExtURLHandleResult, error) { if result == nil || goja.IsUndefined(result) || goja.IsNull(result) { @@ -800,6 +822,29 @@ type PostProcessInput struct { IsSAF bool `json:"is_saf,omitempty"` } +func postProcessInputMap(input PostProcessInput) map[string]any { + result := make(map[string]any, 6) + if input.Path != "" { + result["path"] = input.Path + } + if input.URI != "" { + result["uri"] = input.URI + } + if input.Name != "" { + result["name"] = input.Name + } + if input.MimeType != "" { + result["mime_type"] = input.MimeType + } + if input.Size != 0 { + result["size"] = input.Size + } + if input.IsSAF { + result["is_saf"] = true + } + return result +} + const PostProcessTimeout = 2 * time.Minute // postProcessCommon backs both PostProcess (V1) and PostProcessV2. V1 probes @@ -814,41 +859,26 @@ func (p *extensionProviderWrapper) postProcessCommon(input PostProcessInput, met return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - metadataJSON, _ := json.Marshal(metadata) - inputJSON, _ := json.Marshal(input) filePath := input.Path perfName := "postProcess" - var script string + var invoke func(*goja.Runtime) (goja.Value, error) if preferV2 { perfName = "postProcessV2" - script = fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined') { - if (typeof extension.postProcessV2 === 'function') { - return extension.postProcessV2(%s, %s, %q); - } - if (typeof extension.postProcess === 'function') { - return extension.postProcess(%q, %s, %q); - } - } - return null; - })() - `, string(inputJSON), string(metadataJSON), hookID, filePath, string(metadataJSON), hookID) + inputMap := postProcessInputMap(input) + invoke = func(vm *goja.Runtime) (goja.Value, error) { + if hasExtensionMethod(vm, "postProcessV2") { + return invokeExtensionMethod(vm, "postProcessV2", inputMap, metadata, hookID) + } + return invokeExtensionMethod(vm, "postProcess", filePath, metadata, hookID) + } } else { - script = fmt.Sprintf(` - (function() { - if (typeof extension !== 'undefined' && typeof extension.postProcess === 'function') { - return extension.postProcess(%q, %s, %q); - } - return null; - })() - `, filePath, string(metadataJSON), hookID) + invoke = extensionMethodInvocation("postProcess", filePath, metadata, hookID) } - result, err := callExtensionScript(p, extCallOpts{ + result, err := callExtension(p, extCallOpts{ perfName: perfName, - script: script, + invoke: invoke, timeout: PostProcessTimeout, timeoutMessage: "postProcess timeout: extension took too long to complete", rawError: true, @@ -898,38 +928,10 @@ func (p *extensionProviderWrapper) FetchLyrics(trackName, artistName, albumName return nil, fmt.Errorf("extension '%s' is disabled", p.extension.ID) } - // Use global variables to avoid JS injection issues with special characters in track/artist names - const trackVar = "__sf_lyrics_track" - const artistVar = "__sf_lyrics_artist" - const albumVar = "__sf_lyrics_album" - const durationVar = "__sf_lyrics_duration" - - const script = ` - (function() { - if (typeof extension !== 'undefined' && typeof extension.fetchLyrics === 'function') { - return extension.fetchLyrics(__sf_lyrics_track, __sf_lyrics_artist, __sf_lyrics_album, __sf_lyrics_duration); - } - return null; - })() - ` - - return callExtensionScript(p, extCallOpts{ + return callExtension(p, extCallOpts{ perfName: "fetchLyrics", - script: script, + invoke: extensionMethodInvocation("fetchLyrics", trackName, artistName, albumName, durationSec), timeout: DefaultJSTimeout, - beforeRun: func() func() { - global := p.vm.GlobalObject() - _ = global.Set(trackVar, trackName) - _ = global.Set(artistVar, artistName) - _ = global.Set(albumVar, albumName) - _ = global.Set(durationVar, durationSec) - return func() { - global.Delete(trackVar) - global.Delete(artistVar) - global.Delete(albumVar) - global.Delete(durationVar) - } - }, }, func(perf *extensionCallPerf, result goja.Value) (*LyricsResponse, error) { if result == nil || goja.IsUndefined(result) || goja.IsNull(result) { return nil, fmt.Errorf("fetchLyrics returned null") diff --git a/go_backend/extension_timeout.go b/go_backend/extension_timeout.go index 943b1dcf..1521c13f 100644 --- a/go_backend/extension_timeout.go +++ b/go_backend/extension_timeout.go @@ -28,9 +28,18 @@ func (e *JSExecutionError) Unwrap() error { var jsInterruptGracePeriod = 5 * time.Second func RunWithTimeoutContext(ctx context.Context, vm *goja.Runtime, script string, timeout time.Duration) (goja.Value, error) { + return runGojaCallWithTimeoutContext(ctx, vm, func() (goja.Value, error) { + return vm.RunString(script) + }, timeout) +} + +func runGojaCallWithTimeoutContext(ctx context.Context, vm *goja.Runtime, call func() (goja.Value, error), timeout time.Duration) (goja.Value, error) { if vm == nil { return nil, fmt.Errorf("extension runtime unavailable") } + if call == nil { + return nil, fmt.Errorf("extension call unavailable") + } if timeout <= 0 { timeout = DefaultJSTimeout @@ -70,7 +79,7 @@ func RunWithTimeoutContext(ctx context.Context, vm *goja.Runtime, script string, } }() - val, err := vm.RunString(script) + val, err := call() resultCh <- result{val, err} }() @@ -139,6 +148,20 @@ func RunWithTimeoutContextAndRecover(ctx context.Context, vm *goja.Runtime, scri return result, err } +func runGojaCallWithTimeoutAndRecover(vm *goja.Runtime, call func() (goja.Value, error), timeout time.Duration) (goja.Value, error) { + return runGojaCallWithTimeoutContextAndRecover(context.Background(), vm, call, timeout) +} + +func runGojaCallWithTimeoutContextAndRecover(ctx context.Context, vm *goja.Runtime, call func() (goja.Value, error), timeout time.Duration) (goja.Value, error) { + result, err := runGojaCallWithTimeoutContext(ctx, vm, call, timeout) + + if vm != nil && !IsRuntimeUnsafeError(err) { + vm.ClearInterrupt() + } + + return result, err +} + func IsRuntimeUnsafeError(err error) bool { jsErr, ok := err.(*JSExecutionError) return ok && jsErr.RuntimeUnsafe