Documentation
¶
Overview ¶
Package clientlink holds Factory's browser-facing duplex connection.
The interfaces here are the ones THIS package calls.
Index ¶
- Constants
- Variables
- func CommandKindFor(method Method) (sessionstore.CommandKind, bool)
- func SessionChannel(tenant sessionwire.TenantID, session sessionwire.SessionID) string
- type Admitter
- type Authenticator
- type Authorizer
- type Clock
- type Config
- type ConnectRequest
- type DemandManager
- type Engine
- func (e *Engine) Admit(ctx context.Context, principal identity.Principal, method Method, data []byte) ([]byte, error)
- func (e *Engine) Authenticate(ctx context.Context, req ConnectRequest) (identity.Principal, error)
- func (e *Engine) Bind(ctx context.Context, principal identity.Principal, channel string) (release func(), err error)
- func (e *Engine) Limits() Limits
- func (e *Engine) ReleaseIdleDemand(ctx context.Context) error
- func (e *Engine) Version() string
- type Handler
- func (h *Handler) CloseSession(tenant sessionwire.TenantID, session sessionwire.SessionID)
- func (h *Handler) Connections() int
- func (h *Handler) Engine() *Engine
- func (h *Handler) PublishSession(tenant sessionwire.TenantID, session sessionwire.SessionID, encoded []byte) error
- func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (h *Handler) Shutdown(ctx context.Context) error
- type Limits
- type Method
Constants ¶
const MinPingInterval = time.Second
MinPingInterval is the shortest cadence the connect reply can carry.
It restates factory.MinClientLinkPingInterval, which this package cannot import without a cycle, and limitsParity in the root package holds the two equal. The reason is in that constant's documentation: the reply expresses the cadence in whole seconds, so a sub-second value arrives as zero and the client is never asked to pong at all.
const ProtocolVersion = "1"
ProtocolVersion is the ClientLink APPLICATION protocol this build speaks.
It is not the transport's wire protocol and not the module version. The transport negotiates its own framing; this is the vocabulary of channels and RPC methods layered on top of it, and it moves when that vocabulary changes incompatibly. A browser bundle served by one Factory build may reconnect to a replica running another, so the version is stated by the CLIENT in its connect data and checked here rather than assumed to match.
Variables ¶
var ErrInvalidConfig = errors.New("clientlink: invalid configuration")
ErrInvalidConfig is the class of every NewHandler rejection.
var ErrNotPublished = errors.New("clientlink: the record was not published")
ErrNotPublished reports a session-channel record the node would not publish.
var ErrUnknownMethod = errors.New("clientlink: unknown rpc method")
ErrUnknownMethod reports an RPC method that is not in the command vocabulary.
var ErrUnreadableRecord = errors.New("clientlink: the accepted record cannot be described")
ErrUnreadableRecord reports a durable record this surface cannot project into a public command status. It is a fault in Factory or in what it depends on, never a decision about the caller's request, and it is deliberately NOT one of the codes a refusal envelope can carry.
var ErrUnroutableChannel = errors.New("clientlink: authorized channel names no session")
ErrUnroutableChannel reports a channel the Authorizer allowed and this build cannot name a session in.
It is a FAULT in the composition, not a decision about the caller, and keeping the two apart is the whole reason it exists. The Authorizer is a SEAM: an implementation deciding subscriptions under a grammar this package does not share could authorize a channel whose session cannot be derived, and there are exactly two wrong answers to that. Taking demand for something else -- the principal's tenant with whatever the string happened to contain -- is the dangerous one. Reporting it as a DENIAL is the quiet one: a denial is terminal for the principal, so a browser entitled to the channel would stop asking for a condition that has nothing to do with its permissions.
A well-formed channel naming ANOTHER TENANT is not this error: no principal is entitled to it through this Factory, so there is no browser the quiet wrong answer could stop, and demandKeyOf reports it as a denial.
FuzzTheDemandGrammarAgreesWithTheAuthorizers holds the production pair to one answer, so this is unreachable through Factory's own authorizer; the arm has a reader because the seam is an interface and a test can implement it.
var ErrUnsupportedProtocol = errors.New("clientlink: unsupported protocol version")
ErrUnsupportedProtocol reports a client speaking a ClientLink application protocol this Factory does not implement.
Functions ¶
func CommandKindFor ¶
func CommandKindFor(method Method) (sessionstore.CommandKind, bool)
CommandKindFor reports the command kind a method admits.
The boolean is false for every method not in the vocabulary, and a caller must treat that as a refusal rather than defaulting to some kind: an unknown method that fell through to a real command kind would let a client name an operation the vocabulary does not contain.
func SessionChannel ¶ added in v0.4.0
func SessionChannel(tenant sessionwire.TenantID, session sessionwire.SessionID) string
SessionChannel is the ClientLink channel a session's viewers subscribe to: `session:{tenant}:{session}`, the grammar demandKeyOf parses and wui's protocol package subscribes to (clientlink.ts sessionChannel).
It is demandKeyOf's INVERSE on every channel demandKeyOf accepts, and that is the only property that matters: a publication must land on exactly the channel a viewer's subscription was authorized and routed by. TestSessionChannelIsTheInverseOfTheDemandGrammar holds the round trip.
Types ¶
type Admitter ¶
type Admitter interface {
AdmitCreate(ctx context.Context, principal identity.Principal, req sessionwire.CreateRequest) (sessionstore.DispositionInboxEntry, bool, error)
AdmitInput(ctx context.Context, principal identity.Principal, req sessionwire.InputRequest) (sessionstore.DispositionInboxEntry, bool, error)
AdmitInterrupt(ctx context.Context, principal identity.Principal, req sessionwire.InterruptRequest) (sessionstore.DispositionInboxEntry, bool, error)
AdmitRestore(ctx context.Context, principal identity.Principal, req sessionwire.RestoreRequest) (sessionstore.DispositionInboxEntry, bool, error)
AdmitGateResponse(ctx context.Context, principal identity.Principal, req sessionwire.GateResponseRequest) (sessionstore.DispositionInboxEntry, bool, error)
}
Admitter is the durable command plane an authorized RPC is routed into.
It is internal/admission's V1 surface, declared here as the narrow interface THIS package calls. Every method takes a Core request type and returns the authoritative SessionInbox record, so nothing about a WebSocket, a frame or an RPC method name reaches the service: specification section 8.1 makes the REST control routes and these RPCs two spellings of ONE admission contract, and a seam carrying a transport type would be the first place they could diverge.
The `bool` every method returns is admission's "this call is the one that accepted it". This edge deliberately does not read it -- see replyFor -- and the seam still declares it, because the seam's shape is the service's: an interface that dropped the result would need an adapter, and an adapter is a second place for the two edges to answer differently.
AdmitLegacyCreate is deliberately ABSENT. Admission refuses every legacy create with runtime_unavailable (ErrLegacyCreateUnsupported) before any durable write, because no runtime this program ships can host a legacy session, and no edge -- REST or ClientLink -- routes to it.
type Authenticator ¶
type Authenticator interface {
// AuthenticateLink returns the principal the connect credential names.
AuthenticateLink(ctx context.Context, token string) (identity.Principal, error)
}
Authenticator derives a principal from a ClientLink connect token.
type Authorizer ¶
type Authorizer interface {
// AuthorizeSubscribe covers session:{tenant}:{session}. Parsing a channel
// name grants nothing: the tenant segment is compared to the principal.
AuthorizeSubscribe(ctx context.Context, principal identity.Principal, channel string) error
// AuthorizeControl covers session.input, session.interrupt and
// gate.respond. See the note on httpapi.Authorizer.AuthorizeControl.
//
// A DENIAL must satisfy errors.Is(err, identity.ErrUnauthorized),
// and that is a requirement rather than a description of the implementation
// that exists. rpcRefusal reads it to answer 103 (permission denied,
// terminal for this principal); an implementation returning a
// differently-typed refusal is answered 100 instead, which centrifuge marks
// TEMPORARY -- a denial dressed as a fault, telling a client to retry an
// operation it may never perform. Anything else the authorizer returns is a
// fault and is correctly answered as one.
AuthorizeControl(ctx context.Context, principal identity.Principal, session sessionwire.SessionID, kind sessionstore.CommandKind) error
}
Authorizer decides subscription and RPC operations on an established link. A throttle wraps identity.ErrRateLimited and receives temporary code 100.
type Clock ¶
type Clock interface {
// AfterFunc runs f after d and returns a stop function reporting whether
// it prevented the call.
AfterFunc(d time.Duration, f func()) (stop func() bool)
}
Clock is the time seam, and this package reads exactly one thing from it: when to run the debounced release.
It declares AfterFunc and NOT Now, because nothing here reads a wall clock. A Now this package never called would be a method every composition supplies identically and no test could distinguish. factory.Clock carries both and is assignable to this without an adapter, which is the property server_test.go's seam pairing holds.
type Config ¶
type Config struct {
// Authenticator verifies the connect credential. A ClientLink token is the
// same material as a bearer credential and is verified by the same
// Verifier, which is what makes reconnecting to another replica work.
Authenticator Authenticator
// Authorizer decides every subscribe and every RPC. A successful handshake
// authorizes nothing beyond itself.
Authorizer Authorizer
// Admitter is the durable command plane every authorized RPC is routed
// into. It is required, and NewEngine refuses a composition without one
// rather than letting a link accept commands it can only refuse.
Admitter Admitter
// Demand is the local delivery-demand plane an authorized subscription
// notifies. It is required: a link that accepted subscriptions without
// recording demand would be a replica serving channels it has asked
// nothing to route to it, and the failure would appear as an empty session
// rather than as a refusal.
Demand DemandManager
// Clock schedules the debounced demand release.
Clock Clock
// Limits bounds this replica's connections.
Limits Limits
// Version is the Factory build identity reported to a connecting client.
Version string
}
Config composes the ClientLink engine.
type ConnectRequest ¶
type ConnectRequest struct {
// Token is the connect credential. It may be empty; see Upgrade.
Token string
// ProtocolVersion is the ClientLink application protocol the client
// speaks. An empty value means the client named none.
ProtocolVersion string
// Upgrade is the operation context the HTTP UPGRADE that carries this
// connection was authenticated under, and Upgraded reports whether there
// was one. The router authenticates /v1/realtime before the handler is
// entered and records the principal and the credential source on the
// request context; the transport hands that context to the handshake,
// and the adapter copies it here so the decision is the Engine's and not
// the adapter's. A handshake reached some other way carries none.
Upgrade internalidentity.OperationContext
Upgraded bool
}
ConnectRequest is what a client presents at handshake, reduced to the fields a decision is made from. Nothing else a client sends is read: a name and a version are recorded by the transport for diagnostics and carry no authority.
type DemandManager ¶
type DemandManager interface {
// Acquire records that this replica has a local subscriber for a session.
Acquire(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID) error
// Release gives that demand back. It is called once per session, after the
// last local DeliveryBinding has been gone for the configured debounce.
Release(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID) error
}
DemandManager is Factory's local delivery-demand plane: which sessions this replica is currently serving to a browser, and therefore which sessions it needs a route to.
It is stated in Core's vocabulary and names no routing, transport or binding type. The implementation A7.2 supplies (`internal/routing`) answers an Acquire with the local route it bound, and that value is not this edge's business: a ClientLink knows that a session has a local subscriber, and nothing about which Host serves it. Composing the two is one adapter, and it is A9.1's, exactly as `routing.Binder`'s doc says of the HostLink pool.
What an error from either method MEANS ¶
A FAULT, and nothing else. A session with no live owner is not a failure of demand -- A7.2 step 1 says a first subscriber must not restore a cold session merely for viewing -- so an implementation that answered "no owner" as an error would make every viewer of an idle session receive a refused subscription. This edge classifies an Acquire failure as an internal, TEMPORARY transport error, which is the correct advertisement for a dependency that could not be reached and the wrong one for a decision about the caller.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the transport-independent half of the ClientLink.
Everything here is a decision about a principal, a channel or a method. Nothing here knows about WebSockets, frames or close codes, so the policy can be driven directly by a test and the transport adapter has nothing to decide.
func (*Engine) Admit ¶
func (e *Engine) Admit(ctx context.Context, principal identity.Principal, method Method, data []byte) ([]byte, error)
Admit decodes, authorizes and durably admits one command RPC.
What the two results MEAN, which is the whole contract ¶
The bytes are the REPLY BODY, and they are returned for every outcome the admission service classified -- an acceptance and a refusal alike. The error is returned only for a condition about which NO admission decision exists: an unknown method, a refused authorization, or a fault. A caller must not collapse the two, because a refusal is a durable answer about the caller's command and a fault is not an answer at all.
A refusal travels as a Core ErrorEnvelope in the reply body rather than as a transport error, and that is REST parity rather than a preference: the REST alternative answers the identical condition with the identical sessionwire.ErrorCode in the identical envelope, and a transport error would force the browser to switch on a second, numeric vocabulary for exactly the nine conditions Core already names. The consumer is written for it -- wui's protocol package resolves an RPC whose data carries `error` and no `command_id` into its Core error hierarchy.
The order, and why decoding precedes authorization ¶
An unknown method is refused first: there is no command kind to ask about, and consulting the authorizer with a zero kind would ask a question whose answer means nothing. Then the body is decoded, because the session an authorization decision is made about is the one the decoded request names -- there is no path segment here to read it from, and re-parsing the same bytes to find it early is what lets the authorized session and the admitted session differ. A body that is not a valid V1 command is therefore refused before any authorization decision, and that discloses nothing: the refusal is derived from the caller's own bytes and from no tenant state whatever.
The authorization here is the EDGE's, and it is not repeated inside this package. admission.Service makes its own control decision for the reason its Authorizer doc gives -- it is a durable mutation boundary reachable by more than one transport -- so a command that reaches the service arrives already authorized and is authorized again there. Adding a third caller is what admission.Authorizer's declaration exists to prevent.
func (*Engine) Authenticate ¶
Authenticate decides one handshake.
The protocol check runs BEFORE the credential is verified, and the order is the answer to what each step may reveal: a client speaking a protocol this build does not implement cannot use a successful authentication for anything, and refusing first means an unsupported bundle does not put load on the credential verifier on every reconnect.
A connect token is not required over a cookie-authenticated upgrade ¶
The embedded WUI sends no connect token: it holds a session COOKIE, which the browser attaches to the WebSocket upgrade and which the router already verified before this handler was entered. Requiring a second credential at the handshake would make realtime unreachable for exactly the client the bundle is built for, and Factory mints no credential of its own that could stand in for one. So when the client presents NO token and the upgrade was authenticated by a COOKIE, the principal the upgrade verified is the connection's principal. That is a reuse of a verification this same authenticator made moments earlier on this same connection, not a new authentication path: nothing here reads a cookie, calls a verifier or consults anything the client sent at the handshake.
The rule is narrower than "any authenticated upgrade", and each clause is a refusal a test drives:
- a NON-EMPTY token is always verified as before, whatever the upgrade carried, so the bearer path is unchanged and a token that disagrees with the cookie is not silently overridden by it;
- an upgrade authenticated by a BEARER header is not reused. A browser cannot set one on an upgrade, so that request is not the WUI's, and a non-browser client holds a token it can present at the handshake;
- an upgrade context carrying an unconstructed principal is not reused, because a zero principal is the shape of a context no authenticated edge built.
A cross-site page cannot reach this: the origin guard runs BEFORE the upgrade and refuses an ambient-credential handshake that carries no Origin or another site's, so a cookie-authenticated upgrade that arrives here was sent by the deployment's own origin.
func (*Engine) Bind ¶
func (e *Engine) Bind(ctx context.Context, principal identity.Principal, channel string) (release func(), err error)
Bind authorizes one subscription and records the DeliveryBinding it creates.
It returns the binding's RELEASE, and that shape is the contract rather than a convenience. A caller cannot release a binding it did not take, cannot release one twice, and cannot release someone else's: there is no key to get wrong. The transport needs exactly that, because it learns a subscription has ended in three different places -- an unsubscribe, a disconnect, and a subscribe the library refused after the callback -- and all three must give back the same binding exactly once.
Authorization and demand are ONE entry point, deliberately. A6.1 had AuthorizeSubscribe as the Engine's whole answer to a subscribe, and adding a second call beside it would mean the channel that was authorized and the session whose demand was taken came from two decisions that could disagree -- which is exactly the defect A6.2 removed from the RPC path, where the authorized session and the admitted session came from two decoders.
The order is authorize, then derive, then notify. Nothing about the session is looked up before the authorization decision, so a refusal discloses nothing; and the demand plane is not told about a session the principal was not allowed to watch.
func (*Engine) ReleaseIdleDemand ¶
ReleaseIdleDemand gives back every demand whose release is merely scheduled, now, and reports what could not be given back.
It is the drain: Handler.Shutdown calls it after the node has closed every link, when every DeliveryBinding is gone and every session is sitting behind its debounce. A replica that stopped at the node would leave the routing plane holding demand for sessions no connection remains to serve.
A session whose bindings are still held is left alone. That is not a half-measure: a live binding means a connection the node did not close, and releasing its demand would be this edge deciding a subscription had ended when the transport says it has not.
Every failure is joined rather than the first returned, for routing.Bindings' reason at its own Close: one session's failure must not hide the demand this replica did not give back on the others.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler is the ClientLink: an embedded Centrifuge node behind an http.Handler, with every Factory decision delegated to the Engine.
Everything the transport can do that Factory has not asked for is off:
client publication no OnPublish handler is registered, so the node
refuses a client publish (measured in A5.1)
compression WebsocketConfig.Compression is left false
history and recovery the zero SubscribeOptions is named explicitly
presence and join/leave the same zero value
Redis and NATS brokers GetBroker and GetPresenceManager are never set, so
the node keeps its in-process memory broker
per-channel batching GetChannelBatchConfig is never set, so the
experimental perChannelWriter is never built
The last two are asserted rather than assumed -- TestTheNodeConfigurationLeavesEveryOptionalMechanismOff reads the constructed Config -- which is the "cheap real version" internal/realtime/transport's package doc deferred to this task, now that a production node constructor exists to read.
func NewHandler ¶
NewHandler builds the node and its HTTP entry point.
The node is RUNNING when this returns. A constructor that left it stopped would compose successfully and then refuse every connection, which is a failure mode a composition test cannot see.
func (*Handler) CloseSession ¶ added in v0.4.0
func (h *Handler) CloseSession(tenant sessionwire.TenantID, session sessionwire.SessionID)
CloseSession removes every viewer's subscription to a session on this replica, server-side, so each one's join repairs from the durable journal.
It is the channel-wide form of routing.Publisher.CloseLink, and is what the relay's fail-closed arms reach once delivery is per channel rather than per link: a viewer that cannot be told where to read from must not be left streaming past a gap. The code is centrifuge's UnsubscribeCodeServer (2000), below the 2500 band in which a client resubscribes on its own, so the viewer's subscription reports unsubscribed and its join -- not the transport -- decides what to do.
It runs on its OWN GOROUTINE and returns at once. A server-side unsubscribe runs this handler's OnUnsubscribe callback, which releases a DeliveryBinding under the Engine's lock, and the relay calling this may be holding a lock an Engine caller is waiting on; doing it inline would put the two in a cycle.
func (*Handler) Connections ¶
Connections reports the ClientLinks this replica currently holds.
func (*Handler) Engine ¶
Engine returns the policy half, for a caller that decides without a socket.
func (*Handler) PublishSession ¶ added in v0.4.0
func (h *Handler) PublishSession(tenant sessionwire.TenantID, session sessionwire.SessionID, encoded []byte) error
PublishSession publishes one encoded session-channel record to EVERY viewer of a session on this replica, once.
Channel-wide, once per record, is the Gap 3 ruling's decision, and what bounds it is the transport: each connection has its own queue, bounded in BYTES by PerConnectionQueueBytes, and a viewer that cannot keep up is disconnected with DisconnectSlow (3008) rather than slowing its peers. That viewer reconnects and repairs from the durable journal. The node runs with no history, so a publication nobody is subscribed to is simply gone -- which is correct, because a viewer's join reads the durable journal after it subscribes.
func (*Handler) ServeHTTP ¶
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP refuses what this surface does not serve, then upgrades.
The protobuf refusal is Factory's, not the transport's. centrifuge's WebsocketHandler selects Protobuf framing from `?format=protobuf`, `?cf_protocol=protobuf` or the `centrifuge-protobuf` subprotocol (centrifuge@v0.39.3/handler_websocket.go:186-213) and offers no way to turn that off, so a browser could select a framing this surface has neither qualified nor measured. It is refused BEFORE the upgrade, with an ordinary HTTP status, because a client that cannot speak our framing cannot be told anything useful over a socket that uses it.
func (*Handler) Shutdown ¶
Shutdown closes every ClientLink and stops the node, then gives back the delivery demand those links were holding.
The order is what makes the second half meaningful rather than decorative: Node.Shutdown closes every client and waits for the hub (centrifuge@v0.39.3/node.go:446-454), and closing a client unsubscribes its channels, so by the time it returns every DeliveryBinding is gone and every session's release is merely SCHEDULED behind the debounce. A replica that stopped there would leave the routing table holding demand for sessions no connection remains to serve -- for a drain, that is a HostLink kept open for nobody until the process exits.
Both failures are joined rather than the first returned: a node that failed to stop must not hide demand this replica did not give back.
type Limits ¶
type Limits struct {
// MaxMessageBytes is the limit on one command RPC's data.
MaxMessageBytes int
// MaxConnections bounds concurrent ClientLinks on this replica.
MaxConnections int
// MaxChannelsPerConnection bounds the session channels one link may hold.
MaxChannelsPerConnection int
// PerConnectionQueueBytes bounds one connection's outbound queue in BYTES.
PerConnectionQueueBytes int
// WriteTimeout bounds one outbound write.
WriteTimeout time.Duration
// PingInterval is how often the server pings an idle connection.
PingInterval time.Duration
// PongTimeout is how long a ping may go unanswered.
PongTimeout time.Duration
// CommandTimeout bounds ONE command admission.
//
// It is the only bound a durable admission has, and the reason it must
// exist here is a property of the pinned transport rather than a
// preference. centrifuge@v0.39.3 dispatches an RPC SYNCHRONOUSLY on the
// connection's read loop (client.go:1916 -> 3048), and the connection
// context is cancelled by the websocket handler's `defer close(ctxCh)`
// (handler_websocket.go:290-294) -- when that loop RETURNS. An in-flight
// admission is what keeps the loop from returning, so nothing about the
// connection can cancel one: not a disconnect, not Handler.Shutdown. It was
// documented as "bounded by the link's lifetime" and was bounded by
// nothing, which made a wedged store or directory two operational failures
// rather than one -- a link hung forever, and, because dispatch is serial,
// every other frame on that link blocked behind it, so a replica could not
// be drained while one admission was stuck.
//
// What it promises is exactly what a context promises, and no more. It is a
// DEADLINE handed to the Admitter, not a guillotine on the reply: a
// dependency that honours its context returns at the deadline, and one that
// ignores it is not bounded by anything here. httpapi.RouteLimits states
// the same limit for the REST edge's own deadline, and this is that
// decision for this edge rather than a second authority for one number --
// the two edges bound different work over different transports.
CommandTimeout time.Duration
// DemandReleaseDebounce is how long a session's last local DeliveryBinding
// must have been gone before this replica gives its delivery demand back.
//
// It exists because a browser's subscription set is not stable: a page
// navigation, a reconnect after a lost link and a re-render each drop a
// subscription and take it again within a moment, and a replica that
// released demand on the first of those pays for a registry read and a
// bind on the second. What the debounce buys is that the *round trip* does
// not happen, not that the release is cheap.
//
// It bounds nothing and it is not a deadline: it is a delay before a call
// that DemandTimeout then bounds. Setting it to a large value keeps a
// HostLink open for a session nobody is watching; setting it very small
// makes an ordinary reconnect cost a rebind. Neither is a correctness
// failure -- demand is local routing state and never authority.
DemandReleaseDebounce time.Duration
// OwnershipPollInterval is the gap between ownership polls for unbound demand.
OwnershipPollInterval time.Duration
// DemandTimeout bounds ONE call into the DemandManager.
//
// It exists for the reason CommandTimeout does, and against the same
// mechanism: a subscribe is dispatched synchronously on the connection's
// read loop, and the connection context is cancelled when that loop
// RETURNS, so an in-flight Acquire is the very thing keeping it alive.
// See CommandTimeout for the measurement.
//
// It bounds the debounced RELEASE too, and there the reason is stronger
// rather than weaker: a release runs on a timer, after the connection that
// caused it is typically gone, so there is no request context in existence
// to inherit a deadline from. A release derived from context.Background
// with no timeout would be bounded by nothing at all.
DemandTimeout time.Duration
}
Limits bounds one replica's ClientLink connections.
It restates factory.ClientLinkLimits for the package that consumes it, in the same way every other internal package declares the interfaces it calls. A9.1 converts; TestClientLinkLimitsAreCarriedWhole holds the two shapes in step by name and type, so a field added to one and not the other, or renamed on one side, fails there rather than configuring a zero.
func (Limits) Validate ¶
Validate reports why these limits may not be used.
It is a SECOND check rather than a redundant one. The root package validates what a deployer supplied; this validates what reached the engine, which is also what a direct in-module caller supplies. A handler built from a zero Limits would otherwise configure a zero-byte queue and a zero ping cadence and fail at run time on a live connection.
type Method ¶
type Method string
Method is a typed ClientLink command RPC.
Commands are RPCs and events are server publications, and the split is not symmetry for its own sake: a command needs a correlated reply carrying the admitted CommandID, and an event needs fan-out to every subscriber of a channel. Client PUBLICATION is disabled outright, so this vocabulary is the only way a browser can ask for anything.
const ( MethodSessionCreate Method = "session.create" MethodSessionInput Method = "session.input" MethodSessionInterrupt Method = "session.interrupt" MethodSessionRestore Method = "session.restore" MethodGateRespond Method = "gate.respond" )
The command vocabulary. Each name is the REST path it is the alternative to, because specification section 8.1 makes the two alternatives obeying one admission contract rather than two surfaces with two spellings.