sio

package module
v0.0.0-...-d11327b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 28, 2023 License: MIT Imports: 17 Imported by: 0

README

Socket.IO - Go

This is a Socket.IO library for Go.

Compatibility

In the near future I plan to support last 2 versions of Go. For now supported Go version is 1.16.

The least supported Socket.IO version is 3.0. If you have an older version of Socket.IO in your other projects, please consider upgrading your dependencies.

JavaScript Socket.IO version Socket.IO protocol revision Engine.IO protocol revision socket.io-go version
0.9.x 1, 2 1, 2 Not supported
1.x, 2.x 3, 4 3 Not supported
3.x, 4.x 5 4 0.x

Design Goals

Q&A

Why use Socket.IO?
I want to send big files.
Sometimes Socket.IO doesn't notice when the network connection is down.

Try reducing pingInterval and pingTimeout.

Guide

Transports

If you are a contributor please see: Developing a Transport

Reserved events

When you call socket.On the handler...

These are reserved events:

connect
connect_error
disconnect
disconnecting

open
error
ping

close
reconnect
reconnect_attempt
reconnect_error
reconnect_failed

JSON

JSON serialization is highly customizable. Under the parser/json/serializer directory there are different packages for JSON serialization:

Name Description Usage
stdjson stdlib's encoding/json. This is the default serializer. README.md
go-json README.md
sonic README.md
fast Conditionally uses sonic or go-json. README.md

Contributing

Contributions are very welcome. Please read this page.

Documentation

Index

Constants

View Source
const (
	DefaultReconnectionDelay            = 1 * time.Second
	DefaultReconnectionDelayMax         = 5 * time.Second
	DefaultRandomizationFactor  float32 = 0.5
)
View Source
const (
	SocketIOProtocolVersion = parser.ProtocolVersion
	EngineIOProtocolVersion = eioparser.ProtocolVersion
)
View Source
const (
	DefaultConnectTimeout           = time.Second * 45
	DefaultMaxDisconnectionDuration = time.Minute * 2
)

Variables

View Source
var (
	NewPrintDebugger = eio.NewPrintDebugger
)

Functions

func IsEventReservedForClient

func IsEventReservedForClient(eventName string) bool

func IsEventReservedForNsp

func IsEventReservedForNsp(eventName string) bool

func IsEventReservedForServer

func IsEventReservedForServer(eventName string) bool

Types

type Binary

type Binary []byte

func (Binary) MarshalJSON

func (b Binary) MarshalJSON() ([]byte, error)

func (Binary) SocketIOBinary

func (b Binary) SocketIOBinary() bool

func (*Binary) UnmarshalJSON

func (b *Binary) UnmarshalJSON(data []byte) error

type BroadcastOperator

type BroadcastOperator = adapter.BroadcastOperator

type ClientSocket

type ClientSocket interface {
	Socket
	ClientSocketEvents

	// Whether the socket will try to reconnect when its Client (manager) connects or reconnects.
	Active() bool

	// Connect the socket.
	Connect()

	// Disconnect the socket (a DISCONNECT packet will be sent).
	Disconnect()

	// Retrieves the Manager.
	Manager() *Manager

	// Setting the authentication data is optional and if used, it must be a JSON object (struct or map).
	// Non-JSON-object authentication data will not accepted, and panic will occur.
	SetAuth(v any)
	// Get the authentication data that was set by `SetAuth`.
	// As you might have guessed, returns nil if authentication data was not set before.
	Auth() (v any)

	Volatile() Emitter
}

type ClientSocketConfig

type ClientSocketConfig struct {
	// Authentication data.
	//
	// This can also be set/overridden using Socket.SetAuth method.
	Auth any

	// The maximum number of retries for the packet to be sent.
	// Above the limit, the packet will be discarded.
	//
	// Using `Infinity` means the delivery guarantee is
	// "at-least-once" (instead of "at-most-once" by default),
	// but a smaller value like 10 should be sufficient in practice.
	Retries int

	// The default timeout used when waiting for an acknowledgement.
	AckTimeout time.Duration
}

type ClientSocketConnectErrorFunc

type ClientSocketConnectErrorFunc func(err error)

type ClientSocketConnectFunc

type ClientSocketConnectFunc func()

type ClientSocketDisconnectFunc

type ClientSocketDisconnectFunc func(reason Reason)

type ClientSocketEvents

type ClientSocketEvents interface {
	OnConnect(f ClientSocketConnectFunc)

	OnceConnect(f ClientSocketConnectFunc)

	OffConnect(f ...ClientSocketConnectFunc)

	OnConnectError(f ClientSocketConnectErrorFunc)

	OnceConnectError(f ClientSocketConnectErrorFunc)

	OffConnectError(f ...ClientSocketConnectErrorFunc)

	OnDisconnect(f ClientSocketDisconnectFunc)

	OnceDisconnect(f ClientSocketDisconnectFunc)

	OffDisconnect(f ...ClientSocketDisconnectFunc)
}

type Debugger

type Debugger = eio.Debugger

type Emitter

type Emitter struct {
	// contains filtered or unexported fields
}

func (*Emitter) Emit

func (e *Emitter) Emit(eventName string, v ...any)

func (*Emitter) Socket

func (e *Emitter) Socket() Socket

func (Emitter) Timeout

func (e Emitter) Timeout(timeout time.Duration) Emitter

func (Emitter) Volatile

func (e Emitter) Volatile() Emitter

type Handshake

type Handshake struct {
	// Date of creation
	Time time.Time

	// Authentication data
	Auth json.RawMessage
}

type InternalError

type InternalError struct {
	// contains filtered or unexported fields
}

This is a wrapper for the errors internal to socket.io.

If you see this error, this means that the problem is neither a network error, nor an error caused by you, but the source of the error is socket.io. Open an issue on GitHub.

func (InternalError) Error

func (e InternalError) Error() string

func (InternalError) Unwrap

func (e InternalError) Unwrap() error

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

func NewManager

func NewManager(url string, config *ManagerConfig) *Manager

This function creates a new Manager for client sockets.

You should create a new Socket using the Socket method of the Manager returned by this function. If you don't do that, server will terminate your connection. See: https://socket.io/docs/v4/server-initialization/#connectTimeout

func (*Manager) Close

func (m *Manager) Close()

func (*Manager) OffAll

func (m *Manager) OffAll()

func (*Manager) OffClose

func (m *Manager) OffClose(_f ...ManagerCloseFunc)

func (*Manager) OffError

func (m *Manager) OffError(_f ...ManagerErrorFunc)

func (*Manager) OffOpen

func (m *Manager) OffOpen(_f ...ManagerOpenFunc)

func (*Manager) OffPing

func (m *Manager) OffPing(_f ...ManagerPingFunc)

func (*Manager) OffReconnect

func (m *Manager) OffReconnect(_f ...ManagerReconnectFunc)

func (*Manager) OffReconnectAttempt

func (m *Manager) OffReconnectAttempt(_f ...ManagerReconnectAttemptFunc)

func (*Manager) OffReconnectError

func (m *Manager) OffReconnectError(_f ...ManagerReconnectErrorFunc)

func (*Manager) OffReconnectFailed

func (m *Manager) OffReconnectFailed(_f ...ManagerReconnectFailedFunc)

func (*Manager) OnClose

func (m *Manager) OnClose(f ManagerCloseFunc)

func (*Manager) OnError

func (m *Manager) OnError(f ManagerErrorFunc)

func (*Manager) OnOpen

func (m *Manager) OnOpen(f ManagerOpenFunc)

func (*Manager) OnPing

func (m *Manager) OnPing(f ManagerPingFunc)

func (*Manager) OnReconnect

func (m *Manager) OnReconnect(f ManagerReconnectFunc)

func (*Manager) OnReconnectAttempt

func (m *Manager) OnReconnectAttempt(f ManagerReconnectAttemptFunc)

func (*Manager) OnReconnectError

func (m *Manager) OnReconnectError(f ManagerReconnectErrorFunc)

func (*Manager) OnReconnectFailed

func (m *Manager) OnReconnectFailed(f ManagerReconnectFailedFunc)

func (*Manager) OnceClose

func (m *Manager) OnceClose(f ManagerCloseFunc)

func (*Manager) OnceError

func (m *Manager) OnceError(f ManagerErrorFunc)

func (*Manager) OnceOpen

func (m *Manager) OnceOpen(f ManagerOpenFunc)

func (*Manager) OncePing

func (m *Manager) OncePing(f ManagerPingFunc)

func (*Manager) OnceReconnect

func (m *Manager) OnceReconnect(f ManagerReconnectFunc)

func (*Manager) OnceReconnectAttempt

func (m *Manager) OnceReconnectAttempt(f ManagerReconnectAttemptFunc)

func (*Manager) OnceReconnectError

func (m *Manager) OnceReconnectError(f ManagerReconnectErrorFunc)

func (*Manager) OnceReconnectFailed

func (m *Manager) OnceReconnectFailed(f ManagerReconnectFailedFunc)

func (*Manager) Open

func (m *Manager) Open()

func (*Manager) Socket

func (m *Manager) Socket(namespace string, config *ClientSocketConfig) ClientSocket

type ManagerCloseFunc

type ManagerCloseFunc func(reason Reason, err error)

type ManagerConfig

type ManagerConfig struct {
	// A creator function for the Socket.IO parser.
	// This function is used for creating a parser.Parser object.
	// You can use a custom parser by changing this variable.
	//
	// By default this function is nil and default JSON parser is used.
	ParserCreator parser.Creator

	// Configuration for the Engine.IO.
	EIO eio.ClientConfig

	// Should we disallow reconnections?
	// Default: false (allow reconnections)
	NoReconnection bool

	// How many reconnection attempts should we try?
	// Default: 0 (Infinite)
	ReconnectionAttempts uint32

	// The time delay between reconnection attempts.
	// Default: 1 second
	ReconnectionDelay *time.Duration

	// The max time delay between reconnection attempts.
	// Default: 5 seconds
	ReconnectionDelayMax *time.Duration

	// Used in the exponential backoff jitter when reconnecting.
	// This value is required to be between 0 and 1
	//
	// Default: 0.5
	RandomizationFactor *float32

	// For debugging purposes. Leave it nil if it is of no use.
	//
	// This only applies to Socket.IO. For Engine.IO, use EIO.Debugger.
	Debugger Debugger
}

type ManagerErrorFunc

type ManagerErrorFunc func(err error)

type ManagerOpenFunc

type ManagerOpenFunc func()

type ManagerPingFunc

type ManagerPingFunc func()

type ManagerReconnectAttemptFunc

type ManagerReconnectAttemptFunc func(attempt uint32)

type ManagerReconnectErrorFunc

type ManagerReconnectErrorFunc func(err error)

type ManagerReconnectFailedFunc

type ManagerReconnectFailedFunc func()

type ManagerReconnectFunc

type ManagerReconnectFunc func(attempt uint32)

type Namespace

type Namespace struct {
	// contains filtered or unexported fields
}

func (*Namespace) Adapter

func (n *Namespace) Adapter() adapter.Adapter

func (*Namespace) Compress

func (n *Namespace) Compress(compress bool) *BroadcastOperator

Compression flag is unused at the moment, thus setting this will have no effect on compression.

func (*Namespace) DisconnectSockets

func (n *Namespace) DisconnectSockets(close bool)

Makes the matching socket instances disconnect from the namespace.

If value of close is true, closes the underlying connection. Otherwise, it just disconnects the namespace.

func (*Namespace) Emit

func (n *Namespace) Emit(eventName string, v ...any)

Emits an event to all connected clients in the given namespace.

func (*Namespace) Except

func (n *Namespace) Except(room ...Room) *BroadcastOperator

Sets a modifier for a subsequent event emission that the event will only be broadcast to clients that have not joined the given rooms.

func (*Namespace) FetchSockets

func (n *Namespace) FetchSockets() []adapter.Socket

Returns the matching socket instances. This method works across a cluster of several Socket.IO servers.

func (*Namespace) In

func (n *Namespace) In(room ...Room) *BroadcastOperator

Alias of To(...)

func (*Namespace) Local

func (n *Namespace) Local() *BroadcastOperator

Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node (when scaling to multiple nodes).

See: https://socket.io/docs/v4/using-multiple-nodes

func (*Namespace) Name

func (n *Namespace) Name() string

func (*Namespace) OffAll

func (n *Namespace) OffAll()

func (*Namespace) OffConnection

func (n *Namespace) OffConnection(_f ...NamespaceConnectionFunc)

func (*Namespace) OffEvent

func (n *Namespace) OffEvent(eventName string, handler ...any)

func (*Namespace) OnConnection

func (n *Namespace) OnConnection(f NamespaceConnectionFunc)

func (*Namespace) OnEvent

func (n *Namespace) OnEvent(eventName string, handler any)

func (*Namespace) OnServerSideEmit

func (n *Namespace) OnServerSideEmit(eventName string, _v ...any)

func (*Namespace) OnceConnection

func (n *Namespace) OnceConnection(f NamespaceConnectionFunc)

func (*Namespace) OnceEvent

func (n *Namespace) OnceEvent(eventName string, handler any)

func (*Namespace) ServerSideEmit

func (n *Namespace) ServerSideEmit(eventName string, _v ...any)

Sends a message to the other Socket.IO servers of the cluster.

func (*Namespace) Sockets

func (n *Namespace) Sockets() []ServerSocket

Gets the sockets of the namespace. Beware that this is local to the current node. For sockets across all nodes, use FetchSockets

func (*Namespace) SocketsJoin

func (n *Namespace) SocketsJoin(room ...Room)

Makes the matching socket instances join the specified rooms.

func (*Namespace) SocketsLeave

func (n *Namespace) SocketsLeave(room ...Room)

Makes the matching socket instances leave the specified rooms.

func (*Namespace) To

func (n *Namespace) To(room ...Room) *BroadcastOperator

Sets a modifier for a subsequent event emission that the event will only be broadcast to clients that have joined the given room.

To emit to multiple rooms, you can call `To` several times.

func (*Namespace) Use

func (n *Namespace) Use(f NspMiddlewareFunc)

type NamespaceConnectionFunc

type NamespaceConnectionFunc func(socket ServerSocket)

type NspMiddlewareFunc

type NspMiddlewareFunc func(socket ServerSocket, handshake *Handshake) error

type Reason

type Reason = eio.Reason
const (
	ReasonIOServerDisconnect Reason = "io server disconnect"
	ReasonIOClientDisconnect Reason = "io client disconnect"
)
const (
	ReasonForcedClose    Reason = eio.ReasonForcedClose
	ReasonTransportClose Reason = eio.ReasonTransportClose
	ReasonTransportError Reason = eio.ReasonTransportError
	ReasonPingTimeout    Reason = eio.ReasonPingTimeout
	ReasonParseError     Reason = eio.ReasonParseError
)
const (
	ReasonServerShuttingDown        Reason = "server shutting down"
	ReasonForcedServerClose         Reason = "forced server close"
	ReasonClientNamespaceDisconnect Reason = "client namespace disconnect"
	ReasonServerNamespaceDisconnect Reason = "server namespace disconnect"
)

type Room

type Room = adapter.Room

type Server

type Server struct {
	// contains filtered or unexported fields
}

func NewServer

func NewServer(config *ServerConfig) *Server

func (*Server) Close

func (s *Server) Close() error

Shut down the server. Server cannot be restarted once it is closed.

func (*Server) Compress

func (s *Server) Compress(compress bool) *BroadcastOperator

Compression flag is unused at the moment, thus setting this will have no effect on compression.

Alias of: s.Of("/").Compress(...)

func (*Server) DisconnectSockets

func (s *Server) DisconnectSockets(close bool)

Makes the matching socket instances disconnect from the namespace.

If value of close is true, closes the underlying connection. Otherwise, it just disconnects the namespace.

Alias of: s.Of("/").DisconnectSockets(...)

func (*Server) Emit

func (s *Server) Emit(eventName string, v ...any)

Emits an event to all connected clients in the given namespace.

Alias of: s.Of("/").Emit(...)

func (*Server) Except

func (s *Server) Except(room ...Room) *BroadcastOperator

Sets a modifier for a subsequent event emission that the event will only be broadcast to clients that have not joined the given rooms.

Alias of: s.Of("/").To(...)

func (*Server) FetchSockets

func (s *Server) FetchSockets(room ...string) []adapter.Socket

Returns the matching socket instances. This method works across a cluster of several Socket.IO servers.

Alias of: s.Of("/").FetchSockets(...)

func (*Server) HTTPWriteTimeout

func (s *Server) HTTPWriteTimeout() time.Duration

func (*Server) In

func (s *Server) In(room ...Room) *BroadcastOperator

Alias of: s.Of("/").In(...)

func (*Server) IsClosed

func (s *Server) IsClosed() bool

func (*Server) Local

func (s *Server) Local() *BroadcastOperator

Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node (when scaling to multiple nodes).

See: https://socket.io/docs/v4/using-multiple-nodes

Alias of: s.Of("/").Local(...)

func (*Server) Of

func (s *Server) Of(namespace string) *Namespace

func (*Server) OffAnyConnection

func (s *Server) OffAnyConnection(_f ...ServerAnyConnectionFunc)

func (*Server) OffConnection

func (s *Server) OffConnection(f ...NamespaceConnectionFunc)

Alias of: s.Of("/").OffConnection(...)

func (*Server) OffNewNamespace

func (s *Server) OffNewNamespace(_f ...ServerNewNamespaceFunc)

func (*Server) OnAnyConnection

func (s *Server) OnAnyConnection(f ServerAnyConnectionFunc)

func (*Server) OnConnection

func (s *Server) OnConnection(f NamespaceConnectionFunc)

Alias of: s.Of("/").OnConnection(...)

func (*Server) OnNewNamespace

func (s *Server) OnNewNamespace(f ServerNewNamespaceFunc)

func (*Server) OnceAnyConnection

func (s *Server) OnceAnyConnection(f ServerAnyConnectionFunc)

func (*Server) OnceConnection

func (s *Server) OnceConnection(f NamespaceConnectionFunc)

Alias of: s.Of("/").OnceConnection(...)

func (*Server) OnceNewNamespace

func (s *Server) OnceNewNamespace(f ServerNewNamespaceFunc)

func (*Server) PollTimeout

func (s *Server) PollTimeout() time.Duration

func (*Server) Run

func (s *Server) Run() error

Start the server.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Server) ServerSideEmit

func (s *Server) ServerSideEmit(eventName string, v ...any)

Sends a message to the other Socket.IO servers of the cluster.

func (*Server) Sockets

func (s *Server) Sockets() []ServerSocket

Gets the sockets of the namespace. Beware that this is local to the current node. For sockets across all nodes, use FetchSockets

Alias of: s.Of("/").Sockets(...)

func (*Server) SocketsJoin

func (s *Server) SocketsJoin(room ...Room)

Makes the matching socket instances leave the specified rooms.

Alias of: s.Of("/").SocketsJoin(...)

func (*Server) SocketsLeave

func (s *Server) SocketsLeave(room ...Room)

Makes the matching socket instances leave the specified rooms.

Alias of: s.Of("/").SocketsLeave(...)

func (*Server) To

func (s *Server) To(room ...Room) *BroadcastOperator

Sets a modifier for a subsequent event emission that the event will only be broadcast to clients that have joined the given room.

To emit to multiple rooms, you can call `To` several times.

Alias of: s.Of("/").To(...)

func (*Server) Use

func (s *Server) Use(f NspMiddlewareFunc)

Alias of: s.Of("/").Use(...)

type ServerAnyConnectionFunc

type ServerAnyConnectionFunc func(namespace string, socket ServerSocket)

type ServerConfig

type ServerConfig struct {
	ParserCreator  parser.Creator
	AdapterCreator adapter.Creator

	EIO eio.ServerConfig

	// Duration to wait before a client without namespace is closed.
	//
	// Default: 45 seconds
	ConnectTimeout time.Duration

	// In order for a client to make a connection to a namespace,
	// the namespace must be created on server via `Server.of`.
	//
	// This option permits the client to create the namespace if it is not already created on server.
	// If this option is disabled, only namespaces created on the server can be connected.
	//
	// Default: false
	AcceptAnyNamespace bool

	ServerConnectionStateRecovery ServerConnectionStateRecovery

	// For debugging purposes. Leave it nil if it is of no use.
	//
	// This only applies to Socket.IO. For Engine.IO, use EIO.Debugger.
	Debugger Debugger
}

type ServerConnectionStateRecovery

type ServerConnectionStateRecovery struct {
	// Enable connection state recovery
	//
	// Default: false
	Enabled bool

	// The backup duration of the sessions and the packets
	//
	// Default: 2 minutes
	MaxDisconnectionDuration time.Duration

	// Whether to execute middlewares upon successful connection state recovery.
	//
	// Default: false
	UseMiddlewares bool
}

type ServerNewNamespaceFunc

type ServerNewNamespaceFunc func(namespace *Namespace)

type ServerSocket

type ServerSocket interface {
	Socket
	ServerSocketEvents

	// Retrieves the underlying Server.
	Server() *Server

	// Retrieves the Namespace this socket is connected to.
	Namespace() *Namespace

	// Join room(s)
	Join(room ...Room)
	// Leave a room
	Leave(room Room)
	// Get a set of all rooms socket was joined to.
	Rooms() mapset.Set[Room]

	// Register a middleware for events.
	//
	// Function signature must be same as with On and Once:
	// func(eventName string, v ...any) error
	Use(f any)

	// Sets a modifier for a subsequent event emission that the event
	// will only be broadcast to clients that have joined the given room.
	//
	// To emit to multiple rooms, you can call To several times.
	To(room ...Room) *BroadcastOperator

	// Alias of To(...)
	In(room ...Room) *BroadcastOperator

	// Sets a modifier for a subsequent event emission that the event
	// will only be broadcast to clients that have not joined the given rooms.
	Except(room ...Room) *BroadcastOperator

	// Sets a modifier for a subsequent event emission that
	// the event data will only be broadcast to the current node.
	Local() *BroadcastOperator

	// Sets a modifier for a subsequent event emission that
	// the event data will only be broadcast to every sockets but the sender.
	Broadcast() *BroadcastOperator

	// Disconnect from namespace.
	//
	// If `close` is true, all namespaces are going to be disconnected (a DISCONNECT packet will be sent),
	// and the underlying Engine.IO connection will be terminated.
	//
	// If `close` is false, only the current namespace will be disconnected (a DISCONNECT packet will be sent),
	// and the underlying Engine.IO connection will be kept open.
	Disconnect(close bool)
}

type ServerSocketDisconnectFunc

type ServerSocketDisconnectFunc func(reason Reason)

type ServerSocketDisconnectingFunc

type ServerSocketDisconnectingFunc func(reason Reason)

type ServerSocketErrorFunc

type ServerSocketErrorFunc func(err error)

type ServerSocketEvents

type ServerSocketEvents interface {
	OnError(f ServerSocketErrorFunc)

	OnceError(f ServerSocketErrorFunc)

	OffError(f ...ServerSocketErrorFunc)

	OnDisconnecting(f ServerSocketDisconnectingFunc)

	OnceDisconnecting(f ServerSocketDisconnectingFunc)

	OffDisconnecting(f ...ServerSocketDisconnectingFunc)

	OnDisconnect(f ServerSocketDisconnectFunc)

	OnceDisconnect(f ServerSocketDisconnectFunc)

	OffDisconnect(f ...ServerSocketDisconnectFunc)
}

type Socket

type Socket interface {
	// Socket ID. For client socket, this may return an empty string if the client hasn't connected yet.
	ID() SocketID

	// Is the socket (currently) connected?
	Connected() bool

	// Whether the connection state was recovered after a
	// temporary disconnection. In that case, any missed packets
	// will be transmitted, the data attribute
	// and the rooms will be restored.
	Recovered() bool

	// Emit a message.
	// If you want to emit a binary data, use sio.Binary instead of []byte.
	Emit(eventName string, v ...any)

	// Return an emitter with timeout set.
	Timeout(timeout time.Duration) Emitter

	// Register an event handler.
	OnEvent(eventName string, handler any)

	// Register a one-time event handler.
	// The handler will run once and will be removed afterwards.
	OnceEvent(eventName string, handler any)

	// Remove an event handler.
	//
	// If you want to remove all handlers of a particular event,
	// provide the eventName and leave the handler nil.
	//
	// Otherwise, provide both the eventName and handler arguments.
	OffEvent(eventName string, handler ...any)

	// Remove all event handlers.
	// Including special event handlers (connect, disconnect, disconnecting, etc.).
	OffAll()
}

type SocketID

type SocketID = adapter.SocketID

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL