mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
HIP-0106's plugin contract, in the framework rather than in 116 repositories. Declaration projects the LIVE ROUTER — every method+pattern, complete by construction, so a route a plugin serves and does not publish is still declared. That is the analytics-ingestion outage class: the published paths were listed, the four ingestion doors were not, and every product beacon in the fleet answered 405. Declaration/Route/(*App).Declaration what a host discovers /.well-known/zip/plugin.json the same value off a running child (*App).Described `<bin> openapi <f>` / `<bin> declare <f>` Tenant the tenancy read, with the real rule Delegate a continuation for another org Ask dial a peer by name, invoke one op (*App).Peer peer ops on their own app, own socket (*App).Ops /healthz + /readyz + /metrics Config.Eager the one fact a router cannot show App.claim a second claim of a prefix is an ERROR zip/runtime -> zip/js a language runtime is an adaptor Four things the reference plugin falsified while being built: - Describe was already taken. zip.Describe(methodPath, Doc) is the doc registry generated code calls. The projection writer is (*App).Described, one call, because argv is read in one place and a main cannot forget to forward it. - zip's control plane is NOT all under /.well-known/. /docs and the MCP path are not, so a prefix filter let a child declare them — paths outside /v1/ that every plugin serves and whose first claimant takes the HOST's document and agent door. App.control now registers and records in one call, so leaking one is impossible rather than unlikely. - OPS_PORT appeared in no Go file in the fleet. HIP-0119 named the paths and nobody implemented them, so conformance meant 116 repos hand-rolling three routes and a listener. It is one sibling app now, brought up by Listen. - There is deliberately NO default ops port. A default made every second app in a process die on "address already in use" — a plugin has two apps in it and a test process has many. The deployment states OPS_PORT; a host states none on a child, so a child correctly owns none. Load's duplicate claim used to return nil while the first registration kept answering: the losing plugin's whole surface served another plugin's 404 and nothing said so. It is now an error naming both claimants, all-or-nothing across a plugin's prefixes, taken before anything is spawned. a.ops was the op registry and is now a.registry — the ONE registry the OpenAPI document, the MCP tool, the CLI command and the plane op are all projected from. a.ops is the ops sibling. Co-authored-by: Hanzo Dev <[email protected]>
62 lines
2.2 KiB
Go
62 lines
2.2 KiB
Go
package zip
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
)
|
|
|
|
// conns is the process's one Conn per peer name. A [Conn] is safe for
|
|
// concurrent use and holds a POOLED transport, so the right lifetime for one is
|
|
// the process, not the call: dialing per call throws the pool away and turns
|
|
// every plane hop into a fresh connect, which on a hot path is the whole cost.
|
|
//
|
|
// Keyed by name rather than by resolved path so that a redeployed peer — same
|
|
// name, same socket, new process — is reached by the same handle: the socket
|
|
// path is the identity (see [SocketPath]), and reconnecting is the transport's
|
|
// job, not the caller's.
|
|
var conns sync.Map // name -> *Conn
|
|
|
|
// Ask invokes one op on a named peer and returns its reply. It is the whole of
|
|
// what a caller needs to reach another service:
|
|
//
|
|
// out, err := zip.Ask[plane.AuthorizeIn, plane.AuthorizeOut](
|
|
// ctx, "billing", plane.FinanceAuthorize, &in)
|
|
//
|
|
// The peer is reached over its canonical socket ([SocketPath]) — no registry, no
|
|
// discovery, no address in the caller's configuration. The op token is the
|
|
// operation's one identity: the same string is its operationId, its MCP tool
|
|
// name and its CLI command.
|
|
//
|
|
// Identity is not invented here. Whatever the edge asserted about the caller
|
|
// rides along when ctx carries a request ([Ctx.Forward]); a background job
|
|
// states one explicitly with [WithCaller]. There is no org argument, because an
|
|
// org in the argument is an org the caller chose.
|
|
//
|
|
// The Conn is kept for the life of the process. Nothing needs closing, and a
|
|
// caller that closes one only drops its idle connections — the next Ask
|
|
// redials.
|
|
func Ask[In, Out any](ctx context.Context, name, op string, in *In) (*Out, error) {
|
|
c, err := peer(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Call[In, Out](ctx, c, op, in)
|
|
}
|
|
|
|
// peer returns the process's Conn to name, dialing on first use. Two callers
|
|
// racing the first Ask both dial; one handle wins and the loser's is dropped
|
|
// unused, which costs nothing because dialing is lazy.
|
|
func peer(name string) (*Conn, error) {
|
|
if c, ok := conns.Load(name); ok {
|
|
return c.(*Conn), nil
|
|
}
|
|
c, err := DialApp(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if got, loaded := conns.LoadOrStore(name, c); loaded {
|
|
return got.(*Conn), nil
|
|
}
|
|
return c, nil
|
|
}
|