Files
zeekayandHanzo Dev 616e4b58d7 a hook is about a Base, and which Base arrives with the event
An extension's hooks were stated once, on the Base the process started with,
so a write to a tenant's Base fired nothing an extension had asked for. The
file says what to do when a record is created; it does not say where, and the
where comes with the event.

So a declaration is recorded as something that can be stated on any Base, and
stated on each as it is constructed — the same place realtime is bound, for
the same reason. A hook still sees only the Base it fired on.

This grants an extension nothing it did not have. A file on disk is the
operator's, and it already ran with the process's authority; what changes is
which data its hooks see. An extension written when there was one Base now
runs against each, which is a behaviour change and is the thing to know.

Routes and crons an extension declares stay on the Base that loaded it: a
route is registered once on a router, and a schedule belongs to the process.

A tenant's Base was bootstrapped and never served, so nothing it scheduled
ever ran — its logs were never cleaned and its database never optimised.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 19:56:46 -07:00

50 lines
1.6 KiB
Go

package core
import "sync"
// AppBindings is what every Base this process opens binds to its own hooks, and
// it is read where a Base is constructed — so a binding registered here reaches
// an org's Base as well as the process's own.
//
// It is the hook counterpart of [AppMigrations]: one list, read in one place, so
// there is a single answer to what a Base does. Anything bound instead at the
// process's front door belongs to the one Base that door opens onto.
//
// Register before the Base that should carry it is constructed. The process's
// own Base is built at startup, so a plugin that registers after that says the
// same thing twice: once on the app it was handed, and once here for every Base
// opened later.
//
// What goes here runs on every Base, which is the whole point and also the
// limit: it may hold nothing that reaches past the Base it is handed.
var AppBindings BindingsList
// BindingsList is an ordered list of functions applied to a newly constructed
// App.
//
// A plugin holds one of its own to collect what it has to say before deciding
// which Bases hear it: Register records a statement, Apply makes it on a Base.
type BindingsList struct {
mu sync.RWMutex
list []func(App)
}
// Register adds fn to what every Base binds.
func (l *BindingsList) Register(fn func(App)) {
l.mu.Lock()
defer l.mu.Unlock()
l.list = append(l.list, fn)
}
// Apply runs each registered binding against app, in registration order.
func (l *BindingsList) Apply(app App) {
l.mu.RLock()
list := l.list
l.mu.RUnlock()
for _, bind := range list {
bind(app)
}
}