286 lines
7.7 KiB
Go
286 lines
7.7 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
)
|
|
|
|
// New initialises a Core instance using the provided options and performs the necessary setup.
|
|
// It is the primary entry point for creating a new Core application.
|
|
//
|
|
// Example:
|
|
//
|
|
// core, err := core.New(
|
|
// core.WithService(&MyService{}),
|
|
// core.WithAssets(assets),
|
|
// )
|
|
func New(opts ...Option) (*Core, error) {
|
|
c := &Core{
|
|
services: make(map[string]any),
|
|
Features: &Features{},
|
|
}
|
|
for _, o := range opts {
|
|
if err := o(c); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if c.serviceLock {
|
|
c.servicesLocked = true
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// WithService creates an Option that registers a service. It automatically discovers
|
|
// the service name from its package path and registers its IPC handler if it
|
|
// implements a method named `HandleIPCEvents`.
|
|
//
|
|
// Example:
|
|
//
|
|
// // In myapp/services/calculator.go
|
|
// package services
|
|
//
|
|
// type Calculator struct{}
|
|
//
|
|
// func (s *Calculator) Add(a, b int) int { return a + b }
|
|
//
|
|
// // In main.go
|
|
// import "myapp/services"
|
|
//
|
|
// core.New(core.WithService(services.NewCalculator))
|
|
func WithService(factory func(*Core) (any, error)) Option {
|
|
return func(c *Core) error {
|
|
serviceInstance, err := factory(c)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("core: failed to create service: %w", err)
|
|
}
|
|
|
|
// --- Service Name Discovery ---
|
|
typeOfService := reflect.TypeOf(serviceInstance)
|
|
if typeOfService.Kind() == reflect.Ptr {
|
|
typeOfService = typeOfService.Elem()
|
|
}
|
|
pkgPath := typeOfService.PkgPath()
|
|
parts := strings.Split(pkgPath, "/")
|
|
name := strings.ToLower(parts[len(parts)-1])
|
|
|
|
// --- IPC Handler Discovery ---
|
|
instanceValue := reflect.ValueOf(serviceInstance)
|
|
handlerMethod := instanceValue.MethodByName("HandleIPCEvents")
|
|
if handlerMethod.IsValid() {
|
|
if handler, ok := handlerMethod.Interface().(func(*Core, Message) error); ok {
|
|
c.RegisterAction(handler)
|
|
}
|
|
}
|
|
|
|
return c.RegisterService(name, serviceInstance)
|
|
}
|
|
}
|
|
|
|
// WithName creates an option that registers a service with a specific name.
|
|
// This is useful when the service name cannot be inferred from the package path,
|
|
// such as when using anonymous functions as factories.
|
|
// Note: Unlike WithService, this does not automatically discover or register
|
|
// IPC handlers. If your service needs IPC handling, implement HandleIPCEvents
|
|
// and register it manually.
|
|
func WithName(name string, factory func(*Core) (any, error)) Option {
|
|
return func(c *Core) error {
|
|
serviceInstance, err := factory(c)
|
|
if err != nil {
|
|
return fmt.Errorf("core: failed to create service '%s': %w", name, err)
|
|
}
|
|
return c.RegisterService(name, serviceInstance)
|
|
}
|
|
}
|
|
|
|
// WithWails creates an Option that injects the Wails application instance into the Core.
|
|
// This is essential for services that need to interact with the Wails runtime.
|
|
func WithWails(app *application.App) Option {
|
|
return func(c *Core) error {
|
|
c.App = app
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithAssets creates an Option that registers the application's embedded assets.
|
|
// This is necessary for the application to be able to serve its frontend.
|
|
func WithAssets(fs embed.FS) Option {
|
|
return func(c *Core) error {
|
|
c.assets = fs
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithServiceLock creates an Option that prevents any further services from being
|
|
// registered after the Core has been initialized. This is a security measure to
|
|
// prevent late-binding of services that could have unintended consequences.
|
|
func WithServiceLock() Option {
|
|
return func(c *Core) error {
|
|
c.serviceLock = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// --- Core Methods ---
|
|
|
|
// ServiceStartup is the entry point for the Core service's startup lifecycle.
|
|
// It is called by Wails when the application starts.
|
|
func (c *Core) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
|
c.serviceMu.RLock()
|
|
startables := append([]Startable(nil), c.startables...)
|
|
c.serviceMu.RUnlock()
|
|
|
|
var agg error
|
|
for _, s := range startables {
|
|
if err := s.OnStartup(ctx); err != nil {
|
|
agg = errors.Join(agg, err)
|
|
}
|
|
}
|
|
|
|
if err := c.ACTION(ActionServiceStartup{}); err != nil {
|
|
agg = errors.Join(agg, err)
|
|
}
|
|
|
|
return agg
|
|
}
|
|
|
|
// ServiceShutdown is the entry point for the Core service's shutdown lifecycle.
|
|
// It is called by Wails when the application shuts down.
|
|
func (c *Core) ServiceShutdown(ctx context.Context) error {
|
|
var agg error
|
|
if err := c.ACTION(ActionServiceShutdown{}); err != nil {
|
|
agg = errors.Join(agg, err)
|
|
}
|
|
|
|
c.serviceMu.RLock()
|
|
stoppables := append([]Stoppable(nil), c.stoppables...)
|
|
c.serviceMu.RUnlock()
|
|
|
|
for i := len(stoppables) - 1; i >= 0; i-- {
|
|
if err := stoppables[i].OnShutdown(ctx); err != nil {
|
|
agg = errors.Join(agg, err)
|
|
}
|
|
}
|
|
|
|
return agg
|
|
}
|
|
|
|
// ACTION dispatches a message to all registered IPC handlers.
|
|
// This is the primary mechanism for services to communicate with each other.
|
|
func (c *Core) ACTION(msg Message) error {
|
|
c.ipcMu.RLock()
|
|
handlers := append([]func(*Core, Message) error(nil), c.ipcHandlers...)
|
|
c.ipcMu.RUnlock()
|
|
|
|
var agg error
|
|
for _, h := range handlers {
|
|
if err := h(c, msg); err != nil {
|
|
agg = fmt.Errorf("%w; %v", agg, err)
|
|
}
|
|
}
|
|
return agg
|
|
}
|
|
|
|
// RegisterAction adds a new IPC handler to the Core.
|
|
func (c *Core) RegisterAction(handler func(*Core, Message) error) {
|
|
c.ipcMu.Lock()
|
|
c.ipcHandlers = append(c.ipcHandlers, handler)
|
|
c.ipcMu.Unlock()
|
|
}
|
|
|
|
// RegisterActions adds multiple IPC handlers to the Core.
|
|
func (c *Core) RegisterActions(handlers ...func(*Core, Message) error) {
|
|
c.ipcMu.Lock()
|
|
c.ipcHandlers = append(c.ipcHandlers, handlers...)
|
|
c.ipcMu.Unlock()
|
|
}
|
|
|
|
// RegisterService adds a new service to the Core.
|
|
func (c *Core) RegisterService(name string, api any) error {
|
|
if c.servicesLocked {
|
|
return fmt.Errorf("core: service %q is not permitted by the serviceLock setting", name)
|
|
}
|
|
if name == "" {
|
|
return errors.New("core: service name cannot be empty")
|
|
}
|
|
c.serviceMu.Lock()
|
|
defer c.serviceMu.Unlock()
|
|
if _, exists := c.services[name]; exists {
|
|
return fmt.Errorf("core: service %q already registered", name)
|
|
}
|
|
c.services[name] = api
|
|
|
|
if s, ok := api.(Startable); ok {
|
|
c.startables = append(c.startables, s)
|
|
}
|
|
if s, ok := api.(Stoppable); ok {
|
|
c.stoppables = append(c.stoppables, s)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Service retrieves a registered service by name.
|
|
// It returns nil if the service is not found.
|
|
func (c *Core) Service(name string) any {
|
|
c.serviceMu.RLock()
|
|
api, ok := c.services[name]
|
|
c.serviceMu.RUnlock()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return api
|
|
}
|
|
|
|
// ServiceFor retrieves a registered service by name and asserts its type to the given interface T.
|
|
func ServiceFor[T any](c *Core, name string) (T, error) {
|
|
var zero T
|
|
raw := c.Service(name)
|
|
if raw == nil {
|
|
return zero, fmt.Errorf("service '%s' not found", name)
|
|
}
|
|
typed, ok := raw.(T)
|
|
if !ok {
|
|
return zero, fmt.Errorf("service '%s' is of type %T, but expected %T", name, raw, zero)
|
|
}
|
|
return typed, nil
|
|
}
|
|
|
|
// MustServiceFor retrieves a registered service by name and asserts its type to the given interface T.
|
|
// It panics if the service is not found or cannot be cast to T.
|
|
func MustServiceFor[T any](c *Core, name string) T {
|
|
svc, err := ServiceFor[T](c, name)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return svc
|
|
}
|
|
|
|
// App returns the global application instance.
|
|
// It panics if the Core has not been initialized.
|
|
func App() *application.App {
|
|
if instance == nil {
|
|
panic("core.App() called before core.Setup() was successfully initialized")
|
|
}
|
|
return instance.App
|
|
}
|
|
|
|
// Config returns the registered Config service.
|
|
func (c *Core) Config() Config {
|
|
cfg := MustServiceFor[Config](c, "config")
|
|
return cfg
|
|
}
|
|
|
|
func (c *Core) Core() *Core { return c }
|
|
|
|
// Assets returns the embedded filesystem containing the application's assets.
|
|
func (c *Core) Assets() embed.FS {
|
|
return c.assets
|
|
}
|