Documentation
¶
Overview ¶
Package ws defines the typed WebSocket message envelope shared by the server upgrade handler (internal/api) and the fanout layer.
Every message exchanged over the /api/v1/ws connection — in both directions — is encoded as a JSON Envelope. Clients send subscription control messages; the server sends push notifications and acknowledgements.
Wire format ¶
All messages are JSON text frames. Binary frames are not used. The top-level structure is always an Envelope:
{
"type": "subscribe",
"subject": "jobs/abc123/tasks",
"payload": { … },
"seq": 42
}
Sequence numbers ¶
- Server TypePush messages carry a per-subject monotonically increasing sequence number assigned by the Hub. The sequence is global across connections so clients can store the last received Seq and pass it as since_seq when reconnecting to replay missed events.
- Client messages carry a seq chosen by the client. The server echoes it in the corresponding TypeAck response so the client can correlate replies.
- seq is 0 for messages where sequence tracking is not meaningful (e.g. TypePing / TypePong, TypeAck, TypeError).
Index ¶
- Constants
- type AckPayload
- type DiagEvent
- type DiagnosticsPush
- type Envelope
- type ErrorPayload
- type Hub
- func (h *Hub) Deregister(clientID string)
- func (h *Hub) NotifyDiag(e DiagEvent)
- func (h *Hub) NotifyJob(e JobEvent)
- func (h *Hub) NotifyLog(e LogEvent)
- func (h *Hub) NotifyTask(e TaskEvent)
- func (h *Hub) NotifyWorker(e WorkerEvent)
- func (h *Hub) Register(clientID string) chan Envelope
- func (h *Hub) Subscribe(clientID, subject string, sinceSeq uint64) error
- func (h *Hub) Unsubscribe(clientID, subject string)
- type JobEvent
- type JobSummaryPush
- type LogEvent
- type MessageType
- type NoopNotifier
- type Notifier
- type SubscribePayload
- type TaskEvent
- type TaskLogPush
- type TaskUpdatePush
- type WorkerEvent
- type WorkerStatusPush
Constants ¶
const ( // SubjectJobs streams aggregate job-summary events for all jobs. SubjectJobs = "jobs" // SubjectJobTasksFmt streams task-level events for a specific job. // Format: "jobs/{job_id}/tasks". SubjectJobTasksFmt = "jobs/%s/tasks" // SubjectWorkers streams worker status events. SubjectWorkers = "workers" // SubjectTaskLogsFmt streams log chunks for a specific task attempt. // Format: "tasks/{task_id}/logs". SubjectTaskLogsFmt = "tasks/%s/logs" // SubjectDiagnostics streams diagnostic (operational) log records from the // server and all workers. Payload is a [DiagnosticsPush]; subscribers // filter by component client-side. SubjectDiagnostics = "diagnostics" )
Subject constants for the subscription system. These are prefix patterns; callers substitute resource IDs as needed.
const JobStatusRemoved = "removed"
JobStatusRemoved is a synthetic JobEvent.Status value (not a persisted [store.JobStatus]) signaling that a job row was hard-deleted — by a manual delete or the job-retention sweep — so subscribers should drop it from their views rather than update it in place.
const WorkerStatusRemoved = "removed"
WorkerStatusRemoved is a synthetic WorkerEvent.Status value (not a persisted [store.WorkerStatus]) signaling that a worker row was hard-deleted — by a manual remove or the offline-retention sweep — so subscribers should drop it from their views rather than update it in place.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AckPayload ¶
type AckPayload struct {
// ClientSeq echoes the seq from the client message being acknowledged.
ClientSeq uint64 `json:"client_seq"`
// Error is non-empty when the acknowledged request failed.
// Clients should treat a non-empty Error as equivalent to [TypeError] for
// the specific operation.
Error string `json:"error,omitempty"`
}
AckPayload is the payload of a TypeAck server message.
type DiagEvent ¶
type DiagEvent struct {
Component string // "server" or "worker:<id>"
Level string // DEBUG|INFO|WARN|ERROR
Msg string
Attrs map[string]string
At time.Time
}
DiagEvent is published when a diagnostic log record is ingested (from the server's own logger or a worker's worker.diag stream). The Hub fans this out to clients subscribed to SubjectDiagnostics.
type DiagnosticsPush ¶
type DiagnosticsPush struct {
Component string `json:"component"`
Level string `json:"level"`
Msg string `json:"msg"`
Attrs map[string]string `json:"attrs,omitempty"`
At time.Time `json:"at"`
}
DiagnosticsPush is the TypePush payload for SubjectDiagnostics subscriptions.
type Envelope ¶
type Envelope struct {
// Type is the message discriminator. Required.
Type MessageType `json:"type"`
// Subject identifies the resource or stream the message concerns.
//
// For client subscribe/unsubscribe: the subscription target
// (e.g. "jobs/abc123/tasks").
//
// For server push: the subject that generated the event.
//
// Omitted for ping/pong, ack, and error messages not tied to a subject.
Subject string `json:"subject,omitempty"`
// Payload carries message-specific data as a raw JSON value.
//
// TypeSubscribe → [SubscribePayload]
// TypePush → subject-specific event struct
// TypeAck → [AckPayload]
// TypeError → [ErrorPayload]
// TypeUnsubscribe, TypePing, TypePong → nil
Payload json.RawMessage `json:"payload,omitempty"`
// Seq is the monotonically increasing sequence number.
//
// TypePush (server → client): a per-subject hub-level counter assigned by
// [Hub]. Starts at 1 and increases globally (not per-connection) so
// clients can pass the last received Seq as since_seq when reconnecting.
//
// TypeAck (server → client): 0; the acknowledged client seq is carried in
// [AckPayload.ClientSeq] instead.
//
// Client messages (TypeSubscribe, TypeUnsubscribe): a client-chosen
// counter echoed back in the corresponding TypeAck so the client can
// correlate responses.
//
// TypePing / TypePong / TypeError: always 0.
Seq uint64 `json:"seq"`
}
Envelope is the single message type used for all WebSocket communication. Every frame sent or received on /api/v1/ws is a JSON-encoded Envelope.
The zero value is not a valid message; Type must always be set.
type ErrorPayload ¶
type ErrorPayload struct {
// Code is a machine-readable error identifier, e.g. "invalid_subject".
Code string `json:"code"`
// Message is a human-readable description of the error.
Message string `json:"message"`
}
ErrorPayload is the payload of a TypeError server message.
type Hub ¶
type Hub struct {
// contains filtered or unexported fields
}
Hub is the WebSocket fan-out hub. It implements Notifier and routes scheduler events to subscribed WebSocket clients.
Create with NewHub; the zero value is not usable.
func (*Hub) Deregister ¶
Deregister removes clientID from all subscriptions and closes its send channel so the send goroutine exits. Safe to call multiple times.
func (*Hub) NotifyDiag ¶
NotifyDiag fans a diagnostic log record to all SubjectDiagnostics subscribers.
func (*Hub) NotifyLog ¶
NotifyLog fans a log chunk to clients subscribed to "tasks/{taskID}/logs". The envelope is always stored in the ring buffer so late subscribers can replay it via since_seq: 0 even when no clients are active at emit time.
func (*Hub) NotifyTask ¶
NotifyTask fans a task-status change to:
- SubjectJobs subscribers (job-summary view of the event)
- "jobs/{jobID}/tasks" subscribers (per-job task detail)
func (*Hub) NotifyWorker ¶
func (h *Hub) NotifyWorker(e WorkerEvent)
NotifyWorker fans a worker-status change to all SubjectWorkers subscribers.
func (*Hub) Register ¶
Register creates a hubClient for clientID and returns its send channel. The caller must start a goroutine that drains the channel and writes frames to the WebSocket connection. Call Hub.Deregister when the connection closes.
Calling Register for an already-registered clientID returns the existing send channel without creating a duplicate (idempotent).
func (*Hub) Subscribe ¶
Subscribe registers clientID for push notifications on subject. If sinceSeq > 0, buffered ring entries with Seq > sinceSeq are replayed into the client's send channel immediately (best-effort; entries may be dropped if the buffer is full).
Returns an error if subject is not a recognized subscription subject or if clientID is not currently registered.
func (*Hub) Unsubscribe ¶
Unsubscribe removes clientID from subject's subscription set. No-op when clientID is not subscribed to subject.
type JobEvent ¶
type JobEvent struct {
JobID string
Name string
Owner string
QueueID string
Status string // store.JobStatus value
UpdatedAt time.Time
}
JobEvent is published when a job transitions to a new aggregate status (running, completed, failed, canceled). The Hub fans this out to clients subscribed to SubjectJobs.
type JobSummaryPush ¶
type JobSummaryPush struct {
JobID string `json:"job_id"`
TaskID string `json:"task_id,omitempty"`
Name string `json:"name,omitempty"`
Owner string `json:"owner,omitempty"`
QueueID string `json:"queue_id,omitempty"`
Status string `json:"status"`
UpdatedAt time.Time `json:"updated_at"`
}
JobSummaryPush is the TypePush payload for SubjectJobs subscriptions.
The same payload type carries two kinds of event on the jobs subject, distinguished by TaskID:
- Job-status changes (from NotifyJob) leave TaskID empty; Status is the job status.
- Task-status changes (from NotifyTask) set TaskID; Status is the task status. List subscribers use TaskID to apply task-count deltas to the parent job row instead of treating it as a job-status change.
type LogEvent ¶
type LogEvent struct {
TaskID string
AttemptID string
SeqNum int64
Stream string // "stdout" or "stderr"
Data string
At time.Time
}
LogEvent is published when a log chunk is persisted by the log-ingestion consumer. The Hub fans this out to clients subscribed to "tasks/{taskID}/logs".
type MessageType ¶
type MessageType string
MessageType is the discriminator field in Envelope.
const ( // TypeSubscribe requests delivery of server-push messages for Subject. // Payload must be a [SubscribePayload]. TypeSubscribe MessageType = "subscribe" // TypeUnsubscribe cancels a previously registered subscription for Subject. // No payload required. TypeUnsubscribe MessageType = "unsubscribe" // TypePing is a client-initiated keep-alive. The server replies with // [TypePong]. No payload; seq is 0. TypePing MessageType = "ping" // TypePush carries a server-initiated event for the subscribed Subject. // Payload shape varies by subject (see subject constants). TypePush MessageType = "push" // TypeAck confirms receipt of a client message. Payload is an [AckPayload] // containing the echoed client seq and an optional error detail. TypeAck MessageType = "ack" // TypeError reports a protocol-level or subscription error to the client. // Payload is an [ErrorPayload]. TypeError MessageType = "error" // TypePong is the server reply to [TypePing]. No payload; seq is 0. TypePong MessageType = "pong" )
type NoopNotifier ¶
type NoopNotifier struct{}
NoopNotifier is a Notifier that discards all events. Use it in tests that exercise the scheduler without a running WebSocket hub.
func (NoopNotifier) NotifyDiag ¶
func (NoopNotifier) NotifyDiag(DiagEvent)
NotifyDiag discards the event.
func (NoopNotifier) NotifyJob ¶
func (NoopNotifier) NotifyJob(JobEvent)
NotifyJob discards the event.
func (NoopNotifier) NotifyLog ¶
func (NoopNotifier) NotifyLog(LogEvent)
NotifyLog discards the event.
func (NoopNotifier) NotifyTask ¶
func (NoopNotifier) NotifyTask(TaskEvent)
NotifyTask discards the event.
func (NoopNotifier) NotifyWorker ¶
func (NoopNotifier) NotifyWorker(WorkerEvent)
NotifyWorker discards the event.
type Notifier ¶
type Notifier interface {
NotifyTask(e TaskEvent)
NotifyWorker(e WorkerEvent)
NotifyLog(e LogEvent)
NotifyJob(e JobEvent)
NotifyDiag(e DiagEvent)
}
Notifier is implemented by Hub. Packages outside the ws package — primarily the scheduler — call these methods after persisting state changes to push live events to subscribed WebSocket clients.
All methods must be safe for concurrent use and must not block the caller.
type SubscribePayload ¶
type SubscribePayload struct {
// SinceSeq instructs the server to replay any buffered push messages with
// seq strictly greater than SinceSeq before streaming live events.
// A value of 0 (the default) replays all buffered messages for the subject
// (hub sequences start at 1, so every buffered entry satisfies seq > 0).
// Pass the last received Seq to resume without replay.
SinceSeq uint64 `json:"since_seq,omitempty"`
}
SubscribePayload is the payload of a TypeSubscribe client message.
type TaskEvent ¶
type TaskEvent struct {
JobID string
TaskID string
Name string
Status string // store.TaskStatus value
WorkerID string // empty when unassigned
UpdatedAt time.Time
}
TaskEvent is published by the scheduler when a task transitions to a new status. The Hub fans this out to:
- clients subscribed to SubjectJobs (job-summary updates)
- clients subscribed to "jobs/{jobID}/tasks" (per-job task updates)
type TaskLogPush ¶
type TaskLogPush struct {
TaskID string `json:"task_id"`
AttemptID string `json:"attempt_id,omitempty"`
SeqNum int64 `json:"seq_num"`
Stream string `json:"stream"`
Data string `json:"data"`
At time.Time `json:"at"`
}
TaskLogPush is the TypePush payload for "tasks/{id}/logs" subscriptions.
type TaskUpdatePush ¶
type TaskUpdatePush struct {
JobID string `json:"job_id"`
TaskID string `json:"task_id"`
Name string `json:"name,omitempty"`
Status string `json:"status"`
WorkerID string `json:"worker_id,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
TaskUpdatePush is the TypePush payload for "jobs/{id}/tasks" subscriptions.
type WorkerEvent ¶
type WorkerEvent struct {
WorkerID string
Name string
Hostname string
FarmID string
Status string // store.WorkerStatus value
}
WorkerEvent is published when a worker registers, sends a heartbeat, deregisters, has its status changed by the heartbeat sweep, or is removed. The Hub fans this out to clients subscribed to SubjectWorkers.
type WorkerStatusPush ¶
type WorkerStatusPush struct {
WorkerID string `json:"worker_id"`
Name string `json:"name,omitempty"`
Hostname string `json:"hostname,omitempty"`
FarmID string `json:"farm_id,omitempty"`
Status string `json:"status"`
}
WorkerStatusPush is the TypePush payload for SubjectWorkers subscriptions.
Its field set and order must mirror WorkerEvent exactly: the hub builds the push via the direct struct conversion WorkerStatusPush(e) (see buildEnvelope call in NotifyWorker). Adding or reordering a field here without doing the same in WorkerEvent breaks that conversion at compile time.