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
111 changes: 109 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,123 @@ maintaining 100% compatibility.

### Requirements

The minimum Go version is `go1.23`.
The minimum Go version is `go1.26`.

### Install

The `forms` package can be added to a project with `go get`.
The `scope` package can be added to a project with `go get`.

```shell
go get -u cattlecloud.net/go/scope@latest
```

### Examples

##### New

```go
ctx := scope.New()
```

##### TTL

```go
ctx, cancel := scope.TTL(5 * time.Second)
// ctx is canceled after 5 seconds
defer cancel()
```

##### Deadline

```go
ctx, cancel := scope.Deadline(time.Now().Add(10 * time.Second))
// ctx is canceled at the specified time
defer cancel()
```

##### Cancelable

```go
ctx, cancel := scope.Cancelable()
// ctx can be canceled manually
defer cancel()
```

##### WithCancel

```go
ctx, cancel := scope.WithCancel(parentCtx)
defer cancel()
```

##### WithTTL

```go
ctx, cancel := scope.WithTTL(parentCtx, 3 * time.Second)
// parentCtx with a 3 second timeout
defer cancel()
```

##### WithValue

```go
ctx := scope.WithValue(parentCtx, "userID", 123)
```

##### Value

```go
userID := scope.Value[int](ctx, "userID")
```

##### Join

```go
ctx1, cancel1 := scope.WithCancel(scope.New())
ctx2, cancel2 := scope.TTL(5 * time.Second)

joined, cancel := scope.Join(ctx1, ctx2)
// joined is canceled when ctx1 or ctx2 is canceled
defer cancel()
defer cancel1()
defer cancel2()
```

###### Deadline

```go
ctx1, _ := scope.Deadline(time.Now().Add(10 * time.Second))
ctx2, _ := scope.Deadline(time.Now().Add(20 * time.Second))

joined, _ := scope.Join(ctx1, ctx2)
deadline, ok := joined.Deadline() // deadline is 10 seconds, ok is true
```

###### Done

```go
joined, cancel := scope.Join(ctx1, ctx2)
<-joined.Done() // blocks until either ctx1 or ctx2 is done
```

###### Err

```go
joined, cancel := scope.Join(ctx1, ctx2)
<-joined.Done()
err := joined.Err() // returns the error from the first canceled context
```

###### Value

```go
ctx1 := scope.WithValue(scope.New(), "key", "value1")
ctx2 := scope.WithValue(scope.New(), "key", "value2")

joined, _ := scope.Join(ctx1, ctx2)
val := joined.Value("key") // returns value1 (ctx1's value is checked first)
```

### License

The `cattlecloud.net/go/scope` module is open source under the [BSD](LICENSE) license.
6 changes: 6 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ func TTL(duration time.Duration) (C, Cancel) {
return context.WithTimeout(New(), duration)
}

// Deadline will create a fresh context not part of any preceding chain of
// values, and will expire at the given expiration time.
func Deadline(expiration time.Time) (C, Cancel) {
return context.WithDeadline(New(), expiration)
}

// Cancelable will create a fresh context not part of any preceding chain of
// values, and includes a Cancel function.
func Cancelable() (C, Cancel) {
Expand Down
135 changes: 135 additions & 0 deletions join.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package scope

import (
"context"
"sync"
"time"
)

// join implements a context that is canceled when either of two
// contexts is canceled. It is a variation on the original implementation
// with race condition bug fixes.
//
// https://github.com/LK4D4/joincontext/blob/master/context.go
type join struct {
once sync.Once
a C
b C
done chan struct{}

lock *sync.Mutex
err error
}

// Join combines two contexts into a single context that is canceled when
// either input context is canceled. The returned context's Deadline,
// Value, and Err methods delegate to the earliest of the two input
// contexts. If either context is already done, the returned context is
// immediately done with that context's error.
func Join(a, b C) (C, Cancel) {
j := &join{
a: a,
b: b,
done: make(chan struct{}),
lock: new(sync.Mutex),
}

// check if either context is already done before spawning the goroutine
select {
case <-a.Done():
j.lock.Lock()
j.err = a.Err()
j.lock.Unlock()

close(j.done)
return j, func() {}

case <-b.Done():
j.lock.Lock()
j.err = b.Err()
j.lock.Unlock()

close(j.done)
return j, func() {}

default:
}

go j.run()
return j, j.cancel
}

// Deadline returns the earliest deadline from either context.
// If neither context has a deadline, ok is false.
func (j *join) Deadline() (deadline time.Time, ok bool) {
a, aok := j.a.Deadline()
if !aok {
return j.b.Deadline()
}

b, bok := j.b.Deadline()
if !bok {
return a, true
}

if b.Before(a) {
return b, true
}

return a, true
}

// Done returns a channel that is closed when either context is done.
func (j *join) Done() <-chan struct{} {
return j.done
}

// Err returns the error from whichever context was canceled first,
// or ErrCanceled if Cancel was called on the joined context.
func (j *join) Err() error {
j.lock.Lock()
defer j.lock.Unlock()
return j.err
}

// Value returns the value associated with key in either context,
// prioritizing the first context's value if present.
func (j *join) Value(key any) any {
v := j.a.Value(key)

if v == nil {
v = j.b.Value(key)
}

return v
}

func (j *join) run() {
select {
case <-j.a.Done():
j.once.Do(func() {
j.lock.Lock()
j.err = j.a.Err()
j.lock.Unlock()
close(j.done)
})
case <-j.b.Done():
j.once.Do(func() {
j.lock.Lock()
j.err = j.b.Err()
j.lock.Unlock()
close(j.done)
})
case <-j.done:
return
}
}

func (j *join) cancel() {
j.once.Do(func() {
j.lock.Lock()
j.err = context.Canceled
j.lock.Unlock()
close(j.done)
})
}
Loading