English | 简体中文
Golaxy Core is the execution kernel and programming-model foundation of the Golaxy Distributed Service Development Framework. It hosts EC (Entity-Component) business objects inside Actor-style serialized Runtime domains and provides lifecycles, prototypes, entity trees, in-process events, add-ins, structured concurrency, and Future continuations.
Core determines how business code executes and who owns state. Framework connects that execution model to configuration, logging, RPC, Gate, GAP/GTP, NATS, ETCD, databases, and other production infrastructure.
- Positioning
- Key capabilities
- Requirements and installation
- Quick start
- Architecture
- Actor + EC execution model
- Lifecycles
- Runtime scheduling and frame loop
- Entities, components, and prototypes
- Async programming
- Events and code generation
- Add-in system
- Context, errors, and shutdown
- Default behavior reference
- Project layout
- Development and verification
- Ecosystem and license
Core is a stateful business execution kernel that can be embedded independently in a Go process. It is also a direct code dependency of the higher-level Framework. The three layers have distinct responsibilities:
| Layer | Primary responsibility | Typical contents |
|---|---|---|
| Golaxy Core | In-process execution, state ownership, and business-object lifecycles | Service, Runtime, Entity, Component, Prototype, Event, Scope, Future, Add-in |
| Golaxy Framework | Distributed service assembly and infrastructure integration | Application bootstrap, configuration, logging, RPC, Gate, GAP/GTP, NATS, ETCD, databases |
| Golaxy Scaffold | Game-project scaffold and build-time tooling | Protobuf code generation for Go/Godot, plus Excel-table schema, code, and data processing |
| Application services | Product services and deployment structure | Player, room, battle, and scene workloads reached through long-lived connections, plus independent HTTP friend, mail, and operations services |
Core itself does not provide network listeners, RPC transport, service discovery, message brokers, database drivers, or a configuration center. Use Framework above Core when those features are required, or implement custom add-ins to integrate external systems.
Friend and mail systems are common HTTP application services, not built-in Scaffold modules. Scaffold bootstraps projects and generates build artifacts; it does not implement product-domain services.
- Stateful game-server objects such as players, rooms, battles, scenes, NPCs, and guilds.
- Simulation, remote-control, digital-twin, and real-time collaboration systems that need stable identities and ordered execution.
- Backends that decompose complex objects into composable components while strictly controlling where concurrent writes happen.
- Long-running processes that need both event-driven tasks and fixed-rate frame updates.
- Not one goroutine per Entity: a Runtime manages a group of entities that share one serialized task queue.
- Not a conventional data-oriented ECS: Core EC centers on object lifecycles and component composition rather than global System queries and batch processing.
- Not a durable Actor system: Core does not provide message journaling, crash recovery, cross-process mailboxes, or automatic state persistence.
- Not a general HTTP/CRUD framework: ordinary stateless requests are often simpler with standard HTTP tools; Core is most useful for long-lived business state that requires ordered updates.
- Service scope: manages the parent Context, shutdown barrier, prototype libraries, global entity index, and service add-ins.
- Actor-style Runtime: serializes tasks, entity lifecycles, and optional frame updates on one running goroutine; synchronous events emitted during that work run on the same goroutine.
- EC business model: Entity supplies identity, scope, and lifecycle; Component supplies composable behavior and state.
- Prototype system: declares entity types, default scope, metadata, and built-in component compositions before constructing instances.
- Entity tree: maintains parent-child relationships within one Runtime, with attach, detach, move, post-order removal, and ordered traversal.
- Async coordination: uses
Submit/Postfor Actor mailbox delivery,Scope/Spawnfor background-task lifetimes,Future/Promisefor one-shot results, andContinueOnfor returning continuations to a Runtime. - Synchronous events: offers an in-process signal/slot system with priorities, recursion policies, managed unbinding, and code generation.
- Add-in extension: distinguishes fixed-startup Service add-ins from hot-pluggable Runtime add-ins.
- Lifecycle, health, and statistics: drives Service, Runtime, Entity, Component, and Add-in lifecycles and exposes Scope, Submit/Post/Frame queue, rejected-wait, and frame statistics.
- Go version: follow
go.mod; the current module targets Go 1.25. - Module path:
git.golaxy.org/core - License: GNU Lesser General Public License v2.1
Install:
go get git.golaxy.org/core@latestThis minimal example creates a Service, declares an Entity Prototype, starts a Runtime without a frame loop, creates an Entity, and cancels the parent Context for an ordered shutdown.
package main
import (
"context"
"log"
"time"
"git.golaxy.org/core"
"git.golaxy.org/core/ec"
"git.golaxy.org/core/runtime"
"git.golaxy.org/core/service"
)
type PlayerState struct {
ec.ComponentBehavior
}
func (p *PlayerState) Awake() {
log.Printf("player %s awake", p.Entity().ID())
}
func main() {
parent, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
svcCtx := service.NewContext(
service.With.Context(parent),
service.With.Name("game"),
service.With.RunningEventCB(func(ctx service.Context, event service.RunningEvent, _ ...any) {
switch event {
case service.RunningEvent_Birth:
core.BuildEntityPT(ctx, "player").
AddComponent(PlayerState{}).
Declare()
case service.RunningEvent_Started:
core.NewRuntime(
runtime.NewContext(
ctx,
runtime.With.Name("player-runtime"),
runtime.With.RunningEventCB(func(ctx runtime.Context, event runtime.RunningEvent, _ ...any) {
if event != runtime.RunningEvent_Started {
return
}
if _, err := core.BuildEntity(ctx, "player").New(); err != nil {
log.Printf("create player: %v", err)
}
cancel()
}),
),
core.With.Runtime.AutoRun(true),
core.With.Runtime.Frame(core.With.Frame.Enabled(false)),
)
}
}),
)
<-core.NewService(svcCtx).Run().Done()
}See core_test.go for more scenario-style examples.
flowchart TB
Host[Host application] --> ServiceObject[core.Service]
ServiceObject --> ServiceContext[service.Context]
ServiceContext --> PrototypeLibraries[EntityLib / ComponentLib]
ServiceContext --> ServiceAddins[Service add-ins]
ServiceContext --> GlobalIndex[Global Entity index]
subgraph RuntimeDomain[Runtime Actor serialized execution domain]
RuntimeObject[core.Runtime] --> RuntimeContext[runtime.Context]
ExternalCaller[External caller] -->|Submit or Post| TaskQueue[Actor mailbox]
FrameScheduler[optional frame scheduler] -->|frame task| TaskQueue
TaskQueue --> RuntimeLoop[Runtime goroutine]
RuntimeLoop --> RuntimeContext
RuntimeContext --> LocalManager[local EntityManager]
RuntimeContext --> EntityTree[EntityTree]
RuntimeContext --> RuntimeAddins[Runtime add-ins]
end
ServiceContext --> RuntimeObject
LocalManager --> Entity[Entity]
Entity --> Component[Components]
LocalManager -.->|Scope_Global| GlobalIndex
subgraph AsyncWork[Scope-owned background work]
LifetimeScope[Service / Runtime / Entity / Component Scope] --> SpawnTask[Spawn goroutine / I/O]
SpawnTask --> Promise[Promise completes Future]
end
ServiceContext --> LifetimeScope
Promise -->|ContinueOn| TaskQueue
| Object | Responsibility | Concurrency model |
|---|---|---|
core.Service |
Drives service startup, heartbeat, shutdown, and Service add-in lifecycles. | Its worker loop runs in a dedicated goroutine. |
service.Context |
Holds service resources, prototype libraries, the global entity index, and the shutdown barrier. | Individual capabilities have specific rules; the global entity index is concurrency-safe. |
core.Runtime |
Drives the task queue, frame loop, GC, entities, and Runtime add-ins. | One Runtime owns one serialized running goroutine. |
runtime.Context |
Exposes Runtime-local objects and current execution capabilities. | Direct local-state access is restricted to the owning Runtime goroutine. |
runtime.ConcurrentContext |
Exposes a Runtime subset usable from other goroutines. | Cross-goroutine work returns through Submit or Post. |
ec.Entity |
A business object with an ID, scope, metadata, component collection, and tree-node state. | Ordinary methods belong to the Runtime serialization domain. |
ec.ConcurrentEntity |
Exposes ID, prototype, Context, and termination state through a concurrent view. | Safe to read across goroutines; mutation must still return to the Runtime. |
ec.Component |
A unit of business behavior or state attached to an Entity. | Lifecycle and state mutation belong to the Runtime serialization domain. |
ec.ConcurrentComponent |
Exposes the component ID, name, concurrent Runtime Context, and Component Lifetime Scope. | Usable across goroutines; does not expose State, Enabled, Entity, or Destroy. |
Concurrent views have an explicit publication boundary: an Entity must have been added successfully to a
Runtime, and a Component must have completed Runtime initialization with its Entity or have been added
dynamically to a managed Entity. Cross-goroutine access during initialization is undefined. A nil
AsyncScope() or empty String() is only a limited safeguard, not an atomic readiness probe.
The Actor boundary in Core is the Runtime, not an individual Entity. A Runtime owns one task queue, one local entity manager, one entity tree, and zero or more entities. They all share the same serialized execution domain.
sequenceDiagram
participant Source as External goroutine / timer / I/O
participant ResultFuture as Future
participant Queue as Runtime Actor mailbox
participant RuntimeLoop as Runtime goroutine
participant Model as Entity / Components
Source->>Queue: Submit or Post
Queue->>RuntimeLoop: Dequeue in order
RuntimeLoop->>Model: Execute logic and mutate state
Model-->>RuntimeLoop: Return async.Result
RuntimeLoop-->>ResultFuture: Complete Submit Future
ResultFuture->>Queue: ContinueOn enqueues continuation
Note over Queue,RuntimeLoop: Frame updates, entity lifecycles, and ordinary calls share one boundary
This has several direct consequences:
- Ordinary tasks, lifecycle callbacks, and frame callbacks do not run concurrently inside one Runtime, so their business state normally needs no locks.
- Placing multiple entities in one Runtime gives them a shared order of execution and a shared single-thread throughput limit.
- Entities do not create goroutines automatically, and there is no independent mailbox per Entity.
- Code outside the Runtime must not directly mutate entities, components, the entity tree, or Runtime add-ins. Use
Submitwhen a result is needed,Postfor fire-and-forget delivery, or a Service call by global Entity ID. - Use
Spawnwith an owning object that provides anAsyncScope()for blocking I/O and independent computation. A background function must not touch Runtime-local state directly; useContinueOnto enqueue result handling back onto the Runtime. Submit,Post, andContinueOnalways enqueue, even when called from the owning Runtime, so the Actor ordering boundary remains consistent.
Every Entity exists in the local EntityManager of its owning Runtime:
Scope_Local: the Entity is visible only through the local Runtime index.Scope_Global: the Entity is also registered in the concurrency-safe Service-level index.service.Context.Submit(entityID, ...)orPost(entityID, ...)resolves aConcurrentEntityfrom the global index, then enqueues work onto the target Runtime.- “Global” in Core means addressable across runtimes within the same Service process. Cross-node addressing belongs to Framework distributed-entity and RPC capabilities.
core.NewService emits Birth synchronously while binding a service.Context. Calling Run starts the worker loop:
Birth → Starting → Started → Heartbeat* → Terminating → Terminated
| Stage | Key behavior |
|---|---|
Birth |
The Service exists; declare prototypes, install Service add-ins, or prepare startup resources here. |
Starting |
Starts prototype watchers, freezes the Service add-in manager, and initializes add-ins in installation order. |
Started |
The Service is running; runtimes are commonly created here. |
Heartbeat |
Emits once per second. |
Terminating |
The Service Context and scope are closed; scope tasks are joined and then the wait-group barrier is closed and joined after this callback, while add-ins are still running. |
Terminated |
Scopes and barriers have joined; ordinary Service add-ins have shut down in reverse order and been unloaded, while retained add-ins remain running. The termination Signal completes after this callback. |
A Service can run only once, and a service.Context can be bound to only one core.Service.
core.NewRuntime emits Birth synchronously while binding a runtime.Context. AutoRun defaults to false; when enabled, Run is called after all Birth handlers finish.
Birth → Starting → Started → Tasks / Frames / GC → Terminating → Terminated
| Stage | Key behavior |
|---|---|
Birth |
The Runtime object and task queue exist; pre-start Runtime add-ins may be installed. |
Starting |
Activates installed Runtime add-ins and connects EntityManager lifecycle events. |
Started |
Entities can be created and activated. Entities added before startup are activated here as well. |
| Running | Serially handles calls, frame tasks, and entity lifecycles, and periodically runs Runtime GC. |
Terminating |
The Context is canceled and the task queue has been closed and drained; entities are then destroyed in reverse order. |
Terminated |
The wait group is empty, Runtime add-ins are stopped, managed event handles are unbound, and the termination Signal completes. |
A runtime.Context can be bound to only one core.Runtime, and a Runtime can run only once.
The one-way Entity state chain is:
Born → Entered → Awaking → Starting → Alive → Leaving → Shutting → Dead → Destroyed
The normal enabled Component path is:
Born → Attached → Awaking → Enabling → Starting → Alive
The Component enable/disable branch and runtime-managed individual-removal path are:
Enabling / Starting / Alive → Idle → Starting → Alive
Detaching → Shutting → Disabling → Dead → Destroyed
Attached is the pending state before a Component enters its Awake stage. During normal activation,
Components advance from Attached to Awaking one at a time. First-touch ordering can advance a
referenced Component early. Awaking begins before the Awake callback and ends when the Runtime
advances the Component to Enabling.
Enabling is the initial enable phase. When a Component is re-enabled from Idle, OnEnable runs while
the current state is still Idle, followed by Starting and Alive; Start does not run again. Ordinary
disable invokes the paired OnDisable in the current active state before entering Idle; Disabling is
reserved for individual removal or destruction with the Entity. Individual removal uses Detaching,
while destruction with the Entity may enter Shutting directly. The full individual-removal path is
advanced only while the Entity is between Awaking and Alive; at other stages the Component is still
removed from the collection, but Runtime lifecycle callbacks are skipped.
Components that have not entered Awaking are not advanced by Entity destruction and remain Attached.
| Object | Activation callbacks | Frame callbacks | Deactivation callbacks |
|---|---|---|---|
| Entity | Awake() → Start() |
Update() → LateUpdate() |
Shut() → Dispose() |
| Component | Awake() → OnEnable() → Start() |
Update() → LateUpdate() |
Shut() → OnDisable() → Dispose() |
Important ordering rules:
- Without first-touch
Awakeordering, EntityAwakeruns before ComponentAwakecallbacks. - Component
Awake,OnEnable, andStartremain separate insertion-order phases.ComponentAwakeOnFirstTouchmay execute a referenced Component's pendingAwakebefore its normal turn, but never advancesOnEnableorStart. EntityStartfollows the Component phases. - During destruction, Entity
Shutruns first. The Runtime then runs separate reverse-insertion-order phases for all ComponentShut,OnDisable, andDisposecallbacks, followed by EntityDispose. Shutis paired only with an enteredStart,Disposeonly with an enteredAwake, andOnDisablewithOnEnable; objects that never entered the matching activation phase skip that deactivation callback.- Adding components dynamically to a running Entity synchronously advances the new component activation flow.
- The Component collection remains structurally mutable after an Entity enters
Leaving. Newly added Components remainAttached; the Runtime does not advance their activation lifecycle. After the Entity entersDead, additions update only the local collection because its Component-manager event table is closed. Destroy()on a managed Entity or removable Component advances removal synchronously on the owning Runtime goroutine. Calling it from a lifecycle callback may run paired deactivation callbacks before the original callback returns.- Entering Entity
Deadcloses the Entity Scope before Component disable/dispose phases and EntityDispose. After index removal,DestroyedcompletesTerminated(). - Entering Component
Deadcloses its Lifetime Scope.SetEnabled(false)does not close the Scope, so the same lifetime remains available after re-enabling.
The Runtime mailbox distinguishes Submit, Post, and internal Frame tasks. One Runtime goroutine executes all three serially.
- The default queue is unbounded.
Capacity=128matters only after switching to bounded mode. Submitallocates a Future. A full bounded queue or a closed queue completes that Future with the enqueue error.Postis a no-Future fire-and-forget path. It synchronously reports enqueue errors such asErrTaskQueueFullandErrTaskQueueClosed, but has no execution result.SubmitDelegate,SubmitDelegateVoid, andPostDelegateretain Delegate / DelegateVoid invocation support.Postenters the mailbox even when called by the owning Runtime, so it can avoid synchronous reentrancy; it does not guarantee next-frame execution. Core currently has no separate deferred/next-frame scheduling semantic.- Shutdown drains tasks that were already accepted, then performs a final GC pass.
Runtime.Stats().TasksexposesAccepted,Queued,Running,Completed,Canceled,Panicked,RejectedClosed, andRejectedFullforSubmit,Post, andFrameindependently.Runtime.Stats().Health.LastProgressTimerecords the most recent task start or completion time. A Service-level monitor can combine it with the per-categoryRunningcounters to detect a Runtime that has stopped making progress; Core does not keep one resident watchdog goroutine per Runtime.
An unbounded queue prevents immediate rejection during a transient burst, but a backlog consumes memory and increases latency. Production systems should monitor Queued, Running, rejection counters, and Health.LastProgressTime, and apply admission control at external entry points.
The frame loop is enabled by default at a target of 30 FPS with no frame limit. Disable it to use a Runtime as a purely message-driven Actor:
core.With.Runtime.Frame(
core.With.Frame.Enabled(false),
)With the frame loop enabled:
- The scheduler puts frame tasks on the same queue, preserving serialization between
Update,LateUpdate, and ordinary calls. - Each frame emits
FrameLoopBeginandFrameUpdateBegin, runs everyUpdateandLateUpdate, then emitsFrameUpdateEndandFrameLoopEnd. - The scheduler waits for the current frame task to finish, so multiple frames never execute concurrently.
- Blocking tasks or expensive frame callbacks reduce actual FPS.
Frame()exposes current FPS, frame counts, and recent timings. - Setting
TotalFrames > 0automatically terminates the Runtime after that number of frames.
Runtime GC runs every 10 seconds by default and once more during shutdown:
runtime.Context.CollectGCcollects objects that implementruntime.GCand currently returnNeedGC() == true.With.Runtime.CustomGCcan run application-specific cleanup after built-in cleanup.- Runtime GC is deferred cleanup for framework objects; it does not replace the Go garbage collector.
Prototypes keep reusable construction definitions in the Service scope:
| Object | Purpose |
|---|---|
ComponentLib |
Registers construction prototypes by fully qualified Go component type; repeated declaration of the same named type reuses the existing object. |
EntityLib |
Registers Entity compositions by business prototype name; redeclaring the same name replaces the previous version. |
ComponentDescriptor |
Configures a built-in component name, removability, and metadata. |
EntityDescriptor |
Configures the Entity instance type, default Scope, Component Awake ordering, component-ID policy, and metadata. |
BuildEntityPT |
Declares an Entity Prototype through a fluent API. |
BuildEntity |
Constructs an Entity from a declared Prototype and adds it to the current Runtime. |
EntityLib and ComponentLib use read-only snapshots for concurrent queries. Their Watch APIs deliver the current snapshot followed by later declarations.
- If no persistent ID is supplied, an ID is generated when the Entity enters a Runtime.
- Both Entity and Prototype default to
Scope_Global. Metacarries business metadata keyed by strings.- Event handles stored in
Managed()are unbound automatically during destruction. Terminated()completes after the Entity reachesDestroyed; it does not mean every task in the Entity Scope has exited.- The owning Runtime performs removal and lifecycle advancement for
Destroy(); when called on the Runtime goroutine, that flow is synchronous.
- One Entity may contain multiple components with the same name.
GetComponentreturns the first, whileGetComponentsreturns all of them. ComponentDescriptor.SetRemovabledeclares the removal policy for a built-in Component; Components added dynamically at runtime default to removable.- Components reuse the Entity ID by default. Enable
ComponentUniqueIDto allocate an independent ID for every Component. SetEnabledchanges the enable flag immediately; an attached Component that has not reachedEnablingonly records the flag and applies it during later activation. Disabling a Component that has entered theOnEnablephase unbinds frame updates and invokesOnDisable; enabling it again invokesOnEnable, but never repeatsStart.AsyncScope()lazily creates the Component Lifetime Scope on first access; removal closes it, while disabling keeps it alive.ConcurrentComponentis the concurrency-safe narrow view of a Component; business state must still be accessed on the Runtime throughSubmit,Post, orContinueOn.ComponentAwakeOnFirstTouchdoes not change normal activation orchestration. During activation, business lookup or dependency injection can execute a referenced Component's pendingAwakebefore its normal turn, allowing Component references to determineAwakeorder without advancingOnEnableorStart.- Event handles stored in
Managed()are unbound automatically during component destruction.
Every Runtime exposes an EntityTree with a virtual forest root:
MakeRootattaches a free Entity to the forest root.AddChild,DetachNode, andMoveNodemanage relationships and prevent cycles.RemoveNoderemoves a subtree relationship in post-order but does not destroy any Entity.- Destroying an Entity automatically removes the tree relationships for that Entity and its subtree.
- Child traversal preserves insertion order and supports forward, reverse, filter, and count operations.
- EntityTree is not concurrency-safe and must be used on the owning Runtime goroutine.
Core separates asynchronous behavior into five independent capabilities instead of making one type carry results, streams, lifecycles, and Actor scheduling at once:
| Capability | Type / API | Semantics |
|---|---|---|
| One-shot result | Promise / Future |
One async.Result, replayable to any number of consumers after completion. |
| Result-free completion | Completer / Signal |
Expresses only “completed”; used for Service, Runtime, and Entity lifecycles. |
| Continuous data | Emitter / Stream |
Multiple Result values with single-consumer semantics; multiple readers compete for items. |
| Background-task lifetime | Scope / Spawn |
Binds goroutine cancellation, rejection after close, joining, and statistics to an owning object. |
| Actor continuation | ContinueOn |
Subscribes to a Future and re-enqueues state mutation onto a target Runtime. |
Future is a non-generic, one-shot, replayable consumer view. Promise is its single-completion producer view:
promise, future := async.NewPromise()
go func() {
value, err := load()
promise.Resolve(async.NewResult(value, err))
}()
result := future.Wait(context.Background())
sameResult := future.Wait(context.Background()) // immediately replays the same resultResolvehas one-completion semantics; only the first completer receivestrue.TryGetreads without blocking,Waitwaits for completion or Context cancellation, andDoneexposes the shared completion channel.OnCompleteregisters a completion subscription. If the Future has completed, the callback runs immediately on the subscribing goroutine.- Completion callbacks run on the completing goroutine outside the state lock and must return quickly. Use
ContinueOnwhen Actor state must change. - A Future stores its result, diagnostic ID, and completion-executor ID directly. It does not start a polling or timeout-checking goroutine.
Signal carries no Result, making it suitable for lifecycles where only completion matters:
terminated := runtime.Run()
if err := terminated.Wait(ctx); err != nil {
return err
}Stream specifically represents continuous sources such as timer ticks and Channel bridges:
ticks := core.Every(ctx, time.Second)
for {
result, ok := ticks.Next(ctx)
if !ok {
break
}
_ = result.Value.(time.Time)
}A Stream is a single-consumer stream, not a broadcast bus. Close wakes blocked producers and closes the data channel safely after registered senders exit. Use event or a higher-level message facility when broadcast is required.
Service, Runtime, Entity, and Component each expose a Lifetime Scope:
flowchart LR
ServiceScope[Service Scope] --> RuntimeScope[Runtime Scope]
RuntimeScope --> EntityScope[Entity Scope]
EntityScope --> ComponentScope[Component Scope]
ComponentScope --> BackgroundTask[Spawn task]
ComponentScope -->|Component removal| CancelTask[Cancel and reject new tasks]
A Scope provides:
- A cancelable Context for owned tasks.
- Rejection of new tasks after the owner closes.
Spawned,Active,Completed,Canceled, andRejectedstatistics.Completion()for joining all registered tasks.
Scope.Close() and Scope.Close(nil) use context.Canceled by default; Scope.Close(err) records a specific cancellation cause, available through scope.Err() or context.Cause(scope.Context()). async.ErrScopeClosed only reports an unavailable Scope or rejection of a new task and does not wrap context.Canceled. A nil Scope is also treated as closed: Err() and the Context cause are both async.ErrScopeClosed, the Context is canceled, and Completion() is complete. Close cannot forcibly kill a goroutine or wait for one to exit; a task must observe the Context it receives, and callers join tasks through Completion(). A Component Scope closes on removal, but not on SetEnabled(false). Entity, Runtime, and Service scopes close with their respective lifecycles.
Service and Runtime scopes are created with their respective Contexts. An Entity creates its Scope once when it binds to its owning Runtime and directly uses the Scope Context as its Entity Context, avoiding an additional cancellation layer. A Component Scope is created lazily on the first AsyncScope() call after its Entity has bound to a Runtime; if the Component has already closed, that first access returns an immediately closed Scope.
Service and Runtime shutdown wait for tasks registered in their own scopes to exit. Entity and Component
destruction synchronously closes the corresponding Scope but does not block the Runtime goroutine while its
tasks exit. Wait on the Signal returned by scope.Completion() from another goroutine when a join is required.
future := core.Spawn(
component,
func(ctx context.Context, _ ...any) async.Result {
data, err := repository.Load(ctx, playerID)
return async.NewResult(data, err)
},
)| API | Future | Execution location | Purpose |
|---|---|---|---|
Submit / SubmitDelegate |
Yes | Target Runtime goroutine | Actor tasks that produce a business result. |
SubmitVoid / SubmitDelegateVoid |
Yes | Target Runtime goroutine | No business value, but execution errors or completion still matter. |
Post / PostDelegate |
No | Target Runtime goroutine | Fire-and-forget messages where only successful enqueue matters. |
Spawn / SpawnVoid |
Yes | New goroutine | Blocking I/O or independent computation; must not mutate Runtime-local state directly. |
ContinueOn and Delegate/Void variants |
Yes | Target Runtime goroutine | Serial Actor-state updates after a Future completes. |
After / At |
Yes | Timer callback | One-shot timed results. |
Every / FromChan |
Stream | Bridge goroutine | Continuous ticks or Channel data. |
The complete “background I/O → Actor continuation” pattern is:
loadFuture := core.Spawn(
component,
func(ctx context.Context, _ ...any) async.Result {
data, err := repository.Load(ctx, playerID)
return async.NewResult(data, err)
},
)
next := core.ContinueOn(
component,
loadFuture,
func(ctx runtime.Context, result async.Result, _ ...any) async.Result {
if result.Error != nil {
return result
}
component.Data = result.Value.(*PlayerData)
return async.NewResult(nil, nil)
},
)ContinueOn checks the selected Scope when subscribing, enqueuing, and immediately before execution. Scope closure, task submission failures, and continuation panics are reported through the returned Future. Future completion triggers a lightweight subscription directly, so a fast RPC response needs no extra waiter goroutine and incurs no polling delay.
Combinators use completion subscriptions, atomic counters, and one-completion guards. They do not start one waiting goroutine per input Future:
| API | Semantics | Empty input |
|---|---|---|
Race |
First completion, whether successful or failed. | ErrNoCandidates |
FirstSuccess |
First success; ErrNoFutureSucceeded if every candidate fails. |
ErrNoCandidates |
All |
Returns []any in input order; fails immediately on any failure. |
Successful empty slice |
AllSettled |
Returns every []Result in input order. |
Successful empty slice |
Zip2 |
Returns async.Pair after both inputs succeed. |
ErrNoCandidates if either argument is nil |
Map |
Synchronously maps one completed result. | Not applicable |
FlatMap |
Flattens a Future selected from one completed result. | Not applicable |
Timeout |
The source, Context cancellation, or duration wins—whichever completes first. | Not applicable |
Combinators cancel only their own subscriptions by default, not source tasks, because a Future may be shared. Close the owning Scope or cancel the supplied Context when the producer itself should stop.
Blocking on a pending Future from the Runtime goroutine would freeze the entire Actor. Core stores a completion-executor ID in each Future and implements wait guards on Runtime and Entity contexts:
- Waiting for a pending Future completed by the same Runtime mailbox immediately returns
runtime.ErrRuntimeSelfWait. - Waiting for any other pending Future with a Runtime Context immediately returns
runtime.ErrBlockingWaitInRuntime. - A completed Future remains safe to read immediately through
TryGetorWait. Runtime.Stats().Health.LastWaitRejectIDretains the most recently rejected Future ID for diagnosis.
This detects known Future self-waits. A business callback, synchronous I/O call, or infinite loop that stalls for another reason is observable when a task category remains Running while Health.LastProgressTime stops advancing, and should be checked by an external monitor.
The event package provides in-process synchronous signal/slot events:
- An event dispatches synchronously on the emitter's current goroutine. It does not enter the Runtime queue or cross a process boundary.
- Subscribers run in ascending
priority; equal priorities preserve binding order. Event,Handle, andManagedHandlesare not concurrency-safe. The caller must serialize their use.Handle.Unbind()precisely removes one binding. Entity, Component, andruntime.ContextManaged()collections can own handles for automatic cleanup.- Subscriber panic recovery can be enabled, with errors reported non-blockingly through an error channel.
- Recursion policies are
Allow,Disallow,Discard,SkipReceived, andReceiveOnce. The default recursion-depth limit is 128.
Use eventc to generate type-safe bind helpers, handlers, emitters, and event tables:
//go:generate go run git.golaxy.org/core/event/eventc event
//go:generate go run git.golaxy.org/core/event/eventc eventtab --name=myEventTabTypical workflow:
- Declare event interfaces in a
*_event.gofile. - Add the required
go:generatedirectives. - Use
+event-gen:*and+event-tab-gen:*comments to adjust generation. - Run
go generate ./....
In-repository references:
The repository also contains stringer directives. Install stringer and ensure $GOBIN or $GOPATH/bin is on PATH before repository-wide generation:
go install golang.org/x/tools/cmd/stringer@latest
go generate ./...Add-ins attach cross-cutting capabilities to a Service or Runtime without hard-coding their implementations into Core.
| Property | Service add-in | Runtime add-in |
|---|---|---|
| Installation window | Before Service enters Starting |
Before startup or while running |
| Manager concurrency | Immutable snapshots support concurrent install, uninstall, and lookup before startup | No concurrency protection; callers must serialize operations, using the Runtime goroutine while running |
| Activation | Initialized in installation order during Service Starting |
Preinstalled items activate during Runtime Starting; runtime installation activates synchronously |
| Uninstall | Available only before startup and does not call Shut; unavailable after the manager freezes |
Can be hot-uninstalled while running |
| Shutdown | Ordinary add-ins close in reverse order; retained add-ins remain registered | Deactivated on uninstall or Runtime shutdown |
| Typical use | Shared configuration, logging, database pools, discovery clients | Runtime-local caches, entity helper indexes, frame-related extensions |
An ordinary Add-in moves one way through Loaded → Running → Unloaded. A Service add-in that
implements service.RetainedAddIn remains Running after Service termination. Lifecycle contracts include:
- General:
LifecycleAddInInit,LifecycleAddInShut - Service:
LifecycleServiceAddInInit,LifecycleServiceAddInShut - Runtime:
LifecycleRuntimeAddInInit,LifecycleRuntimeAddInShut - Runtime event subscription:
LifecycleAddInOnRuntimeRunningEvent
Service add-ins finish Init in installation order before the Starting callback, after which the
manager remains frozen. Service shutdown first joins the Service scope and wait group, then calls
Shut on ordinary add-ins in reverse installation order. During Shut, an add-in remains Running
and registered with the manager; it is removed and becomes Unloaded only after the callback returns.
Install a dependency before its ordinary dependents so it shuts down last.
service.RetainedAddIn is a Service-only marker. An implementing add-in skips Shut and removal,
remains available through Require after Terminated, and is eventually collected with the Service
Context. It must tolerate a canceled Service Context and must not own background work or external
resources that require explicit shutdown. Ordinary add-ins must stop and join private work or resources
that are not attached to the Service scope or wait group. Runtime add-ins do not support retention.
The define package declares type-safe add-in definitions and exposes consistent Install, Uninstall, Require, and Lookup operations:
Requirereturns only an add-in inRunningstate and panics when unavailable.Lookuprequires only that the manager still holds the add-in; it does not guarantee activation.- The default name is the fully qualified interface or instance type name. Its ID is an FNV-1a hash of that name.
- A Service Context derives from
context.Background()by default. - A Runtime Context derives from its owning Service Context by default.
- An Entity Scope derives directly from its owning Runtime Context, and the Entity Context reuses that Scope Context.
- Service, Runtime, Entity, and Component expose separate Lifetime
AsyncScope()values. Component scopes are created on demand, and lower-level scopes are canceled with their parent Context. - Parent cancellation propagates downward, but
Terminated()completes only after the corresponding object finishes cleanup.
Service and Runtime contexts both carry a WaitGroup barrier:
Join(delta)registers work that must finish before host shutdown.- Once cleanup begins, the barrier closes and rejects new positive increments.
Done()completes one registered unit.Terminate()requests cancellation;Terminated()means cleanup has actually finished.- Starting a Runtime automatically joins its parent Service barrier, so Service waits for all runtimes to exit.
Use the WaitGroup for host-level external resources. Prefer AsyncScope for new background business tasks. Service and Runtime shutdown close and join their scopes before closing and waiting on the legacy barrier. Ordinary Service add-ins shut down only after both have joined, so tasks attached to the Service scope and runtimes joined to the Service barrier cannot outlive the add-in shutdown phase. Retained add-ins do not receive Shut.
Service and Runtime contexts default to AutoRecover=false. With PanicHandling(true, reportError), framework-managed lifecycle, task, and event callbacks attempt to recover panics and write stack-bearing errors non-blockingly to reportError.
Recovery prevents the worker loop from failing immediately; it does not make a partially executed business operation transactional. Callbacks should still preserve explicit invariants and be designed for failure.
UnsafeContext, UnsafeEntity, UnsafeRuntime, and similar entry points exist for internal assembly, generated code, and advanced integrations. They may bypass lifecycle or threading boundaries and are not the preferred APIs for ordinary business code.
| Setting | Default | Notes |
|---|---|---|
| Service parent Context | context.Background() |
Used when no parent is supplied. |
| Runtime parent Context | Owning Service Context | Service cancellation propagates to Runtime. |
AutoRecover |
false |
Panics propagate by default. |
| Service / Runtime persistent ID | Generated | Uses uid.ID. |
| Entity Scope | Scope_Global |
Enters both the Runtime-local and Service-global indexes. |
| Entity persistent ID | Generated when entering Runtime | May be overridden during construction. |
Component first-touch Awake |
false |
When enabled, Component access during activation may advance only the target's pending Awake; later phases are unchanged. |
| Independent Component IDs | false |
Components reuse the Entity ID by default. |
Runtime AutoRun |
false |
Call Run explicitly or enable AutoRun. |
| Frame loop | Enabled | Target is 30 FPS by default. |
| Frame limit | 0 |
Unlimited. |
| Task queue | Unbounded | Bounded mode has a default capacity parameter of 128. |
| Runtime GC interval | 10 seconds | Also runs once during shutdown. |
| Service heartbeat | 1 second | Emits RunningEvent_Heartbeat. |
| Event recursion | Allow |
Maximum depth is 128. |
.
├── define/ # Type-safe add-in definitions
├── ec/ # Entity, Component, state machines, and entity events
│ └── pt/ # Entity / Component prototypes and concurrent libraries
├── event/ # Synchronous events, handles, recursion control, and eventc
├── extension/ # Add-in contracts shared by Service and Runtime
├── runtime/ # Runtime Context, calls, EntityManager, and EntityTree
├── service/ # Service Context, global entity index, and Service add-ins
├── utils/ # async, corectx, generic, iface, meta, uid, and other utilities
├── async.go # Submit/Post, Spawn, timer, and stream entry points
├── continue.go # Future-to-Runtime Actor continuations
├── runtime*.go # Runtime loop, frame, task queue, GC, and lifecycles
└── service*.go # Service loop and lifecycle
| Package | Responsibility |
|---|---|
/ |
Public entry points, Service/Runtime drivers, lifecycle contracts, entity builders, and async helpers. |
/service |
Service Context, prototype access, global entity index, cross-Runtime entity calls, and Service add-ins. |
/runtime |
Runtime Context, task scheduling, frame statistics, local EntityManager, EntityTree, and Runtime add-ins. |
/ec |
Entity/Component model, concurrent narrow views, state machines, component management, scopes, and tree-node events. |
/ec/pt |
Entity/Component Prototypes, descriptors, concurrent libraries, and instance construction. |
/event |
Synchronous events, priorities, recursion policies, Handle, ManagedHandles, and event tables. |
/event/eventc |
Type-safe event code generator used through go:generate. |
/extension |
Common Add-in contracts, states, installation, lookup, and dependency helpers. |
/define |
Generic Service, Runtime, and common Add-in definitions. |
/utils/async |
Result, Promise/Future, Signal, Stream, Scope, and waiter-free combinators. |
/utils/corectx |
Shared Service/Runtime Context, AsyncScope, wait-group, and shutdown protocol. |
/utils |
Generic containers, interface caches, metadata, options, type helpers, and UIDs. |
Regular checks:
go test ./...
go vet ./...For concurrency-sensitive changes, also run:
go test -race ./...core_stress_test.go uses the stress build tag and runs for 120 seconds by default:
go test -tags stress .Override the stress duration:
go test -tags stress . -args "-stress.duration=10s"Generate event, event-table, and enum-string code:
go generate ./...Before committing, confirm that generated *.gen.go and *_string.go files are up to date, and avoid introducing blocking I/O on a Runtime goroutine.
- Golaxy Framework: distributed communication, RPC, Gate, protocol stack, and infrastructure integration built on Core.
- Golaxy Scaffold: game-project scaffold centered on Protobuf generation and Excel-table processing.
- Golaxy Examples: end-to-end examples.
This project is licensed under the GNU Lesser General Public License v2.1.