Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 42 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -848,51 +848,6 @@ crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</div>
```

### Deploy your application
Leveraging Go's built-in `//go:embed` directive and the standard library's `fs.FS` interface, we can compile all static assets and configuration files into a single self-contained binary. This dependency-free approach enables seamless deployment to any server environment.

```go

//go:embed app
var fsys embed.FS

func main() {
var dev bool
flag.BoolVar(&dev, "dev", false, "it is development environment")

flag.Parse()

var opts []xun.Option
if dev {
// use local filesystem in development, and watch files to reload automatically
opts = []xun.Option{xun.WithFsys(os.DirFS("./app")), xun.WithWatch()}
} else {
// use embed resources in production environment
views, _ := fs.Sub(fsys, "app")
opts = []xun.Option{xun.WithFsys(views)}
}

app := xun.New(opts...)
//...

app.Start()
defer app.Close()

if dev {
slog.Default().Info("xun-admin is running in development")
} else {
slog.Default().Info("xun-admin is running in production")
}

err := http.ListenAndServe(":80", http.DefaultServeMux)
if err != nil {
panic(err)
}
}
```



### Works with [tailwindcss](https://tailwindcss.com/docs/installation)
#### 1. Install Tailwind CSS
Install tailwindcss via npm, and create your tailwind.config.js file.
Expand Down Expand Up @@ -1138,6 +1093,48 @@ create an `admin` group router, and apply a middleware to check if it's logged.
})
```

## Deploy your application
Leveraging Go's built-in `//go:embed` directive and the standard library's `fs.FS` interface, we can compile all static assets and configuration files into a single self-contained binary. This dependency-free approach enables seamless deployment to any server environment.

```go

//go:embed app
var fsys embed.FS

func main() {
var dev bool
flag.BoolVar(&dev, "dev", false, "it is development environment")

flag.Parse()

var opts []xun.Option
if dev {
// use local filesystem in development, and watch files to reload automatically
opts = []xun.Option{xun.WithFsys(os.DirFS("./app")), xun.WithWatch()}
} else {
// use embed resources in production environment
views, _ := fs.Sub(fsys, "app")
opts = []xun.Option{xun.WithFsys(views)}
}

app := xun.New(opts...)
//...

app.Start()
defer app.Close()

if dev {
slog.Default().Info("xun-admin is running in development")
} else {
slog.Default().Info("xun-admin is running in production")
}

err := http.ListenAndServe(":80", http.DefaultServeMux)
if err != nil {
panic(err)
}
}
```

## Contributing
Contributions are welcome! If you're interested in contributing, please feel free to [contribute to Xun](CONTRIBUTING.md)
Expand Down
14 changes: 7 additions & 7 deletions ext/sse/client.go → ext/sse/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ var (
ErrClientClosed = errors.New("sse: client closed")
)

// Client represents a connection to a streaming service.
// Conn represents a connection to a streaming service.
// It holds the client's ID, a Streamer instance for managing the stream,
// a context for cancellation and timeout, and a channel for signaling closure.
type Client struct {
type Conn struct {
ID string
s Streamer
ctx context.Context
Expand All @@ -22,20 +22,20 @@ type Client struct {
// Connect establishes a connection for the Client using the provided Streamer.
// It assigns the Streamer to the Client's rw field and ensures that it implements
// the http.Flusher interface for flushing data.
func (c *Client) Connect(ctx context.Context, s Streamer) {
func (c *Conn) Connect(ctx context.Context, s Streamer) {
c.s = s
c.ctx, c.cancel = context.WithCancel(ctx)
}

// Send sends an event to the client by writing the event name and data to the response writer.
// It marshals the event data into JSON format and flushes the output to ensure the data is sent immediately.
// This method is part of the Client struct and is intended for use in server-sent events (SSE) communication.
func (c *Client) Send(event Event) error {
func (c *Conn) Send(evt Event) error {
select {
case <-c.ctx.Done():
return NewError(c.ID, ErrClientClosed)
default:
err := event.Write(c.s)
err := evt.Write(c.s)
if err != nil {
return NewError(c.ID, err)
}
Expand All @@ -49,12 +49,12 @@ func (c *Client) Send(event Event) error {
// Wait blocks until the context is done or the client is closed.
// It listens for either the cancellation of the context or a signal
// to close the client, allowing for graceful shutdown.
func (c *Client) Wait() {
func (c *Conn) Wait() {
<-c.ctx.Done()
}

// Close gracefully shuts down the Client by sending a signal to the close channel.
// This method should be called to ensure that any ongoing operations are properly terminated.
func (c *Client) Close() {
func (c *Conn) Close() {
c.cancel()
}
121 changes: 104 additions & 17 deletions ext/sse/event.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
package sse

import (
"bytes"
"encoding/json"
"fmt"
"io"
"sync"

"strconv"
"strings"
)

var (
bufPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}

sbPool = sync.Pool{
New: func() interface{} {
return &strings.Builder{}
},
}
)

// Event represents an interface for writing event data to an io.Writer.
// Implementations of this interface must provide the Write method,
// which takes an io.Writer and returns an error if the write operation fails.
Expand All @@ -17,52 +34,122 @@ type Event interface {
// TextEvent represents a simple event structure with a name and associated data.
// It is used to encapsulate information for events in the SSE (Server-Sent Events) protocol.
type TextEvent struct {
Name string
Data string
ID string
Name string
Retry int
Data string
}

// Write formats the TextEvent as a string and writes it to the provided io.Writer.
// It outputs the event name and data in the SSE format, followed by two newlines.
// Returns an error if the write operation fails.
func (e *TextEvent) Write(w io.Writer) error {
// Write event header.
var b strings.Builder
b.WriteString("event: ")
b.WriteString(e.Name)
b.WriteString("\n")
sb := sbPool.Get().(*strings.Builder)
defer sbPool.Put(sb)
sb.Reset()

if e.ID != "" {
sb.WriteString("id: ")
sb.WriteString(e.ID)
sb.WriteString("\n")
}

if e.Retry > 0 {
sb.WriteString("retry: ")
sb.WriteString(strconv.Itoa(e.Retry))
sb.WriteString("\n")
}

if e.Name != "" {
sb.WriteString("event: ")
sb.WriteString(e.Name)
sb.WriteString("\n")
}

// Split the data into lines.
lines := strings.Split(e.Data, "\n")
// Build the SSE response.
for _, line := range lines {
b.WriteString("data: ")
b.WriteString(line)
b.WriteString("\n")
sb.WriteString("data: ")
sb.WriteString(line)
sb.WriteString("\n")
}
b.WriteString("data:\n\n")

sb.WriteString("\n")

// Write the complete output.
_, err := io.WriteString(w, b.String())
_, err := io.WriteString(w, sb.String())
return err
}

// JsonEvent represents an event with a name and associated data.
// It can be used to structure events in a JSON format in the SSE (Server-Sent Events) protocol.
type JsonEvent struct {
Name string
Data any
ID string
Name string
Retry int
Data any
}

// Write serializes the JsonEvent to the provided io.Writer in the SSE format.
// It writes the event name and the JSON-encoded data, followed by a double newline
// to indicate the end of the event. If an error occurs during marshaling or writing,
// it returns the error.
func (e *JsonEvent) Write(w io.Writer) error {
buf, err := json.Marshal(e.Data)
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
buf.Reset()

err := json.NewEncoder(buf).Encode(e.Data)

if err != nil {
return err
}
_, err = fmt.Fprintf(w, "event: %s\ndata: %s\n\n", e.Name, string(buf))

sb := sbPool.Get().(*strings.Builder)
defer sbPool.Put(sb)
sb.Reset()

if e.ID != "" {
sb.WriteString("id: ")
sb.WriteString(e.ID)
sb.WriteString("\n")
}

if e.Retry > 0 {
sb.WriteString("retry: ")
sb.WriteString(strconv.Itoa(e.Retry))
sb.WriteString("\n")
}

if e.Name != "" {
sb.WriteString("event: ")
sb.WriteString(e.Name)
sb.WriteString("\n")
}

// Split the data into lines.
lines := strings.Split(buf.String(), "\n")
// Build the SSE response.
for _, line := range lines {
if line != "" {
sb.WriteString("data: ")
sb.WriteString(line)
sb.WriteString("\n")
}
}

sb.WriteString("\n")

// Write the complete output.
_, err = io.WriteString(w, sb.String())
return err
}

type PingEvent struct {
}

func (evt *PingEvent) Write(w io.Writer) error {
_, err := io.WriteString(w, ": ping\n\n")
return err
}
67 changes: 67 additions & 0 deletions ext/sse/reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package sse

import (
"bufio"
"io"
"strconv"
"strings"
)

type EventReader struct {
r io.Reader
}

func NewReader(r io.Reader) *EventReader {
return &EventReader{r: r}
}

func (r *EventReader) Next() (TextEvent, error) {

reader := bufio.NewReader(r.r)

var (
buf []byte
line string
err error

evt TextEvent
retry int
)

for {
buf, err = reader.ReadBytes('\n')
if err != nil {
if err != io.EOF {
return evt, err
}

Check warning on line 36 in ext/sse/reader.go

View check run for this annotation

Codecov / codecov/patch

ext/sse/reader.go#L35-L36

Added lines #L35 - L36 were not covered by tests

return evt, io.EOF
}

line = string(buf)
if strings.HasPrefix(line, ":") {
continue
}

if line == "\n" {
return evt, nil
}

if strings.HasPrefix(line, "id:") {
evt.ID = strings.TrimSpace(line[3:])
} else if strings.HasPrefix(line, "event:") {
evt.Name = strings.TrimSpace(line[6:])
} else if strings.HasPrefix(line, "retry:") {
retry, err = strconv.Atoi(strings.TrimSpace(line[6:]))
if err == nil {
evt.Retry = retry
}
} else if strings.HasPrefix(line, "data:") {
if evt.Data == "" {
evt.Data = strings.TrimSpace(line[5:])
} else {
evt.Data += "\n" + strings.TrimSpace(line[5:])
}

Check warning on line 64 in ext/sse/reader.go

View check run for this annotation

Codecov / codecov/patch

ext/sse/reader.go#L63-L64

Added lines #L63 - L64 were not covered by tests
}
}
}
Loading