timeutil

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 5 Imported by: 0

README

timeutil

Go Reference Tests Coverage Status CodeQL

timeutil is a small Go package that provides serializable timers with automatic callback execution. The timers behave like the standard time.AfterFunc, but their deterministic state (start time, duration, and current state) can be snapshotted and serialized for persistence, inspection, or transfer between processes.

Features

  • Snapshot state — marshal and unmarshal timer state to/from JSON.
  • Automatic callback execution — real time.Timer runs in the background; no manual polling is required.
  • State-aware — tracks running, stopped, and expired states.
  • AfterFunc support — create timers that execute a callback when they expire.
  • Thread-safe — all public methods are safe for concurrent use.
  • Snapshot support — export lightweight TimerSnapshot values for persistence outside JSON.
  • Watchdog timer — a separate helper that fires after a period of inactivity unless reset.

Installation

go get github.com/ghettovoice/timeutil

Requires Go 1.25 or later.

Usage

Basic timer
package main

import (
    "fmt"
    "time"

    "github.com/ghettovoice/timeutil"
)

func main() {
    // Create a new timer that expires after 5 seconds.
    timer := timeutil.NewTimer(5 * time.Second)

    // Check whether the timer has expired.
    if timer.Expired() {
        fmt.Println("Timer has expired")
    }

    // Time remaining.
    fmt.Println("Time remaining:", timer.Left())

    // Time elapsed.
    fmt.Println("Time elapsed:", timer.Elapsed())
}
Timer with a callback
timer := timeutil.AfterFunc(5*time.Second, func() {
    fmt.Println("Timer expired!")
})

// Or set a callback after creation.
timer := timeutil.NewTimer(5 * time.Second)
timer.SetCallback(func() {
    fmt.Println("Timer expired!")
})

SetCallback automatically executes the callback immediately if the timer has already expired, and it starts a real time.Timer for timers that are still running.

Serialization
// Create a timer.
timer := timeutil.NewTimer(10 * time.Second)

// Serialize to JSON.
data, err := timer.ToJSON()
if err != nil {
    panic(err)
}

// Restore later.
restored, err := timeutil.FromJSON(data)
if err != nil {
    panic(err)
}

// Callbacks are not serialized; reattach them after restoration.
restored.SetCallback(func() {
    fmt.Println("Restored timer expired!")
})

The serialized representation is the same as TimerSnapshot:

{
  "start_time": "2025-11-03T12:54:05.184256+03:00",
  "duration": 5000000000,
  "state": "running",
  "stop_time": "2025-11-03T12:54:07.184256+03:00"
}
  • start_time — ISO 8601 timestamp when the timer started.
  • duration — duration in nanoseconds.
  • state — one of running, stopped, or expired.
  • stop_time — present only for stopped timers.
Recreating a timer from a specific start time

Use FromTime when you need to recreate a timer from saved timing metadata:

startTime := time.Now().Add(-2 * time.Second)
timer := timeutil.FromTime(startTime, 5*time.Second)

// UpdateState checks whether the timer has already expired and triggers the
// callback if one is set.
timer.UpdateState()
Watchdog timer
watchdog := timeutil.NewWatchdog(30*time.Second, func() {
    fmt.Println("No activity for 30 seconds")
})
watchdog.Start()

// Later, to keep the watchdog from firing:
watchdog.Reset()

// To pause it while keeping it reusable:
watchdog.Stop()

watchdog.Close()

NewWatchdog creates an inactive watchdog; Start or Reset arms it, and a nonpositive timeout leaves it permanently disabled. Reset always begins a fresh full countdown, including on a stopped, expired, or never-started watchdog, and a nil callback is allowed. Stop pauses the watchdog while keeping it reusable; Close disables it permanently.

API overview

Timer

Creation:

  • NewTimer(duration) — create and start a timer immediately.
  • AfterFunc(duration, callback) — create a timer that executes a callback on expiration.
  • FromTime(startTime, duration) — recreate a timer with a specific start time.
  • FromJSON(data) — deserialize a timer from JSON.

State queries:

  • State() — current timer state.
  • StartTime() — when the timer started.
  • Duration() — configured duration.
  • StopTime() — when the timer was stopped (zero value if not stopped).
  • Expired() — whether the timer has expired.
  • Elapsed() — elapsed time since start.
  • Left() — remaining time until expiration.

Control:

  • Stop() — stop the timer and prevent callback execution.
  • Reset(duration) — restart the timer with a new duration, preserving any callback.
  • SetCallback(callback) — attach or replace the expiration callback.
  • UpdateState() — re-check expiration and trigger callbacks if needed.

Serialization:

  • Snapshot() — capture the current state as a TimerSnapshot.
  • SnapshotTimer(timer) — same as timer.Snapshot(), but safe on nil.
  • RestoreTimer(snapshot) — create a new timer from a snapshot.
  • ToJSON() — serialize to JSON.
  • Implements json.Marshaler and json.Unmarshaler.
Watchdog
  • NewWatchdog(timeout, callback) — create an inactive watchdog.
  • Start() — arm the watchdog to fire after timeout; no-op while already armed.
  • Reset() — restart a fresh timeout countdown, including on stopped or expired watchdogs.
  • Stop() — cancel the current timer; the watchdog stays reusable via Start or Reset.
  • Close() — disable the watchdog permanently.

Important notes

  1. Automatic execution — when a callback is set, a real time.Timer runs in the background, so UpdateState() is usually not needed.
  2. UpdateState() is called automatically during JSON unmarshaling, so timers restored with FromJSON already reflect the current time.
  3. SetCallback() checks for expiration automatically — it runs the callback immediately if the timer is already expired.
  4. Call UpdateState() manually only when you need to re-check expiration after time has passed, or after creating a timer with FromTime before a callback was set.
  5. Callbacks are executed in their own goroutine, like time.AfterFunc.
  6. Callbacks are not serialized and must be reattached after restoration.
  7. Left() returns 0 for stopped or expired timers.
  8. Reset() preserves any existing callback but restarts the real timer with the new duration.
  9. Watchdog Stop() and Close() return without waiting for an already admitted callback: once admitted for execution, it may still begin or continue after they return, so callers must coordinate separately; callbacks from different armed generations may overlap.

Thread safety

All public methods on Timer and Watchdog are safe for concurrent use from multiple goroutines without external synchronization.

Contributing

Contributions are welcome. Please make sure all checks pass before submitting a pull request:

task check

License

This project is licensed under the MIT License. See the LICENSE file for details.

Documentation

Overview

Package timeutil provides serializable timer types that behave like time.AfterFunc but can be snapshotted, serialized, and restored across process restarts or long-lived workflows.

The main type is Timer, which keeps its runtime behaviour, including automatic callback execution via a background time.Timer, while exposing deterministic state through TimerSnapshot. Snapshots can be marshaled to JSON, stored, and later passed to RestoreTimer to obtain a fresh timer instance. Callbacks are runtime-only; they must be reattached after restoration with Timer.SetCallback or Timer.Reset.

Watchdog is created inactive and armed with Watchdog.Start or Watchdog.Reset. Watchdog.Stop allows reuse; Watchdog.Close permanently disables it.

Basic usage:

// Create timer with automatic callback execution.
timer := timeutil.AfterFunc(5*time.Second, func() {
    log.Println("Timer expired!")
})

// Persist the timer state.
snap := timer.Snapshot()
data, _ := json.Marshal(snap)

// Restore later and reattach callbacks.
var restoredSnap timeutil.TimerSnapshot
_ = json.Unmarshal(data, &restoredSnap)
restored, _ := timeutil.RestoreTimer(&restoredSnap)
restored.SetCallback(func() {
    log.Println("Restored timer expired!")
})

All timer operations are thread-safe and can be called concurrently from multiple goroutines.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Error

type Error string

Error is a package-level error type used for simple sentinel errors.

const (
	// ErrInvalidTimerSnapshot is returned when a snapshot is nil or contains
	// invalid timer state and cannot be used to restore a timer.
	ErrInvalidTimerSnapshot Error = "invalid timer snapshot"
)

func (Error) Error

func (e Error) Error() string

Error implements the [error] interface.

type Timer

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

Timer represents a timer that can be serialized to/from JSON. It tracks the start time, duration, and current state and can export/import a lightweight TimerSnapshot for storage. Runtime-only fields such as callbacks and the underlying time.Timer are intentionally excluded from the snapshot and must be reattached manually after restoration. Timer automatically manages a real time.Timer for callback execution while it is running.

Example
package main

import (
	"fmt"
	"time"

	"github.com/ghettovoice/timeutil"
)

func main() {
	// Create a new timer that expires after a short duration.
	// In production code this would typically be several seconds.
	timer := timeutil.NewTimer(10 * time.Millisecond)

	// Set a callback to execute when timer expires
	timer.SetCallback(func() {
		// Handle timeout: send response, cleanup, etc.
	})

	// Check timer state
	if timer.Expired() {
		// Timer has expired, apply additional actions
		fmt.Println("timer expired!")
	} else {
		fmt.Println("timer is still running")
	}

	// Get remaining time
	left := timer.Left()
	if left > 0 {
		// Timer is still running
	}

	// Serialize timer for persistence
	data, _ := timer.ToJSON()

	// Wait for expiration
	time.Sleep(20 * time.Millisecond)

	// Later, restore the timer
	restoredTimer, _ := timeutil.FromJSON(data)

	// Set callback for restored timer
	restoredTimer.SetCallback(func() {
		fmt.Println("callback fired!")
	})

	if restoredTimer.Expired() {
		// Timer expired while serialized, handle appropriately
		fmt.Println("restored timer expired!")
	}

	time.Sleep(10 * time.Millisecond)

}
Output:
timer is still running
restored timer expired!
callback fired!

func AfterFunc

func AfterFunc(duration time.Duration, f func()) *Timer

AfterFunc creates a new Timer with the given duration and callback. The timer is started immediately and the callback will be executed when it expires.

Example
package main

import (
	"fmt"
	"sync/atomic"
	"time"

	"github.com/ghettovoice/timeutil"
)

func main() {
	var fired int32

	// AfterFunc starts the timer and executes the callback in its own goroutine
	// when the timer expires.
	timer := timeutil.AfterFunc(10*time.Millisecond, func() {
		atomic.AddInt32(&fired, 1)
	})

	// Wait for the real timer to fire.
	time.Sleep(25 * time.Millisecond)

	fmt.Println(timer.Expired())
	fmt.Println(atomic.LoadInt32(&fired) > 0)

}
Output:
true
true

func FromJSON

func FromJSON(data []byte) (*Timer, error)

FromJSON deserializes a timer from a JSON string.

func FromTime

func FromTime(startTime time.Time, duration time.Duration) *Timer

FromTime creates a new Timer with the given start time and duration. This is useful for recreating timers from serialized data. Unlike FromJSON, this does not automatically call UpdateState(). You should call UpdateState() after creating the timer to check expiration and trigger callbacks.

Example
package main

import (
	"fmt"
	"time"

	"github.com/ghettovoice/timeutil"
)

func main() {
	// Recreate a timer that started 5 seconds ago with a 1 second duration.
	start := time.Now().Add(-5 * time.Second)
	timer := timeutil.FromTime(start, time.Second)

	// UpdateState recalculates the timer state and triggers callbacks if needed.
	timer.UpdateState()

	fmt.Println(timer.Expired())
	fmt.Println(timer.State())

}
Output:
true
expired

func NewTimer

func NewTimer(duration time.Duration) *Timer

NewTimer creates a new Timer with the given duration. The timer is started immediately.

func RestoreTimer

func RestoreTimer(snap *TimerSnapshot) (*Timer, error)

RestoreTimer recreates a Timer from its snapshot. It returns an error if snap is nil or contains an invalid state. Callback-related fields are left nil; callers should reattach callbacks or restart timers using [SetCallback] / [Reset] as appropriate after restoration.

Example
package main

import (
	"fmt"
	"time"

	"github.com/ghettovoice/timeutil"
)

func main() {
	// A snapshot captures only deterministic state; it can be stored or restored
	// at any time.
	snap := &timeutil.TimerSnapshot{
		StartTime: time.Now().Add(-5 * time.Second),
		Duration:  time.Second,
		State:     timeutil.TimerStateExpired,
	}

	timer, err := timeutil.RestoreTimer(snap)
	if err != nil {
		panic(err)
	}

	fmt.Println(timer.Expired())

}
Output:
true

func (*Timer) Duration

func (t *Timer) Duration() time.Duration

Duration returns the timer's duration.

func (*Timer) Elapsed

func (t *Timer) Elapsed() time.Duration

Elapsed returns the time elapsed since the timer started.

func (*Timer) Expired

func (t *Timer) Expired() bool

Expired returns true if the timer has expired.

func (*Timer) Left

func (t *Timer) Left() time.Duration

Left returns the time remaining until the timer expires. Returns 0 if the timer is expired or stopped.

func (*Timer) MarshalJSON

func (t *Timer) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (*Timer) Reset

func (t *Timer) Reset(duration time.Duration)

Reset resets the timer with a new duration, starting from now. The callback is preserved - if one was set, it will execute when the new duration expires. To clear the callback, call Stop() first.

func (*Timer) SetCallback

func (t *Timer) SetCallback(f func())

SetCallback sets a function to be executed when the timer expires. Similar to time.AfterFunc, the function is called in its own goroutine. If the timer has already expired, the function will be executed immediately. If the timer is stopped, the function will not be executed. This method automatically starts a real time.Timer to handle callback execution.

func (*Timer) Snapshot

func (t *Timer) Snapshot() *TimerSnapshot

Snapshot returns a serializable copy of the timer state. The returned snapshot can be serialized directly or passed to RestoreTimer to recreate a timer instance with the same timing metadata.

func (*Timer) StartTime

func (t *Timer) StartTime() time.Time

StartTime returns the timer's start time.

func (*Timer) State

func (t *Timer) State() TimerState

State returns the current timer state in a thread-safe manner.

func (*Timer) Stop

func (t *Timer) Stop() bool

Stop stops the timer and updates its state. If the timer is stopped, the callback will not be executed.

func (*Timer) StopTime

func (t *Timer) StopTime() time.Time

StopTime returns the timer's stop time (zero value if not stopped).

func (*Timer) ToJSON

func (t *Timer) ToJSON() ([]byte, error)

ToJSON serializes the timer to a JSON string.

func (*Timer) UnmarshalJSON

func (t *Timer) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

func (*Timer) UpdateState

func (t *Timer) UpdateState()

UpdateState updates the timer's state based on the current time. This is useful for timers created with FromTime or when the caller wants to manually re-check expiration after a period of inactivity.

type TimerSnapshot

type TimerSnapshot struct {
	State     TimerState    `json:"state"`
	StartTime time.Time     `json:"start_time"`
	Duration  time.Duration `json:"duration"`
	StopTime  time.Time     `json:"stop_time,omitzero"`
}

TimerSnapshot represents a serializable view of a timer. Only deterministic fields are included so that the snapshot can be safely persisted or transferred between goroutines or processes.

func SnapshotTimer

func SnapshotTimer(t *Timer) *TimerSnapshot

SnapshotTimer safely snapshots the provided timer.

func (*TimerSnapshot) IsValid

func (s *TimerSnapshot) IsValid() bool

IsValid reports whether the snapshot contains a known timer state.

func (*TimerSnapshot) Validate

func (s *TimerSnapshot) Validate() error

Validate returns an error if the snapshot is nil or contains an invalid state, zero start time, or an inconsistent stop time.

type TimerState

type TimerState uint

TimerState represents the current state of a serializable timer.

const (
	// TimerStateInvalid is the zero value and represents an unknown or invalid state.
	TimerStateInvalid TimerState = iota
	// TimerStateRunning indicates that the timer is active and counting down.
	TimerStateRunning
	// TimerStateStopped indicates that the timer was stopped before expiration.
	TimerStateStopped
	// TimerStateExpired indicates that the timer has reached its expiration time.
	TimerStateExpired
)

func TimerStateFromString

func TimerStateFromString(s string) TimerState

TimerStateFromString parses a timer state from its textual representation. It returns TimerStateInvalid when the input does not match a known state.

func (TimerState) AppendText

func (s TimerState) AppendText(b []byte) ([]byte, error)

AppendText appends the textual representation of the state to b.

func (TimerState) IsValid

func (s TimerState) IsValid() bool

IsValid reports whether the state is one of the defined timer states.

func (TimerState) MarshalText

func (s TimerState) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler for JSON/text serialization.

func (TimerState) String

func (s TimerState) String() string

String returns the textual representation of the state, or "invalid" for an unknown state.

func (*TimerState) UnmarshalText

func (s *TimerState) UnmarshalText(data []byte) error

UnmarshalText implements encoding.TextUnmarshaler for JSON/text deserialization.

type Watchdog

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

Watchdog invokes a callback after an explicitly started timeout unless reset.

All methods are concurrent-safe and are no-ops on a nil receiver; a zero Watchdog is valid and permanently disabled. Stop allows reuse, while Close permanently disables the watchdog.

Callbacks execute outside the watchdog's mutex and may overlap across armed generations; callers must synchronize callback access to shared state.

Example
package main

import (
	"fmt"
	"time"

	"github.com/ghettovoice/timeutil"
)

func main() {
	expired := make(chan struct{})

	// NewWatchdog creates an inactive watchdog; Start arms it to fire after
	// the configured inactivity period unless it is reset or stopped.
	watchdog := timeutil.NewWatchdog(10*time.Millisecond, func() { close(expired) })
	defer watchdog.Close()

	// Start arms the watchdog without extending an already running countdown.
	watchdog.Start()
	<-expired
	fmt.Println("expired")

}
Output:
expired

func NewWatchdog

func NewWatchdog(timeout time.Duration, callback func()) *Watchdog

NewWatchdog creates an inactive watchdog timer; call Start or Reset to arm it. A nonpositive timeout leaves the watchdog permanently disabled. A nil callback is permitted; expiration then invokes nothing.

func (*Watchdog) Close added in v0.2.0

func (t *Watchdog) Close()

Close permanently disables the watchdog and releases the stored callback. Close is idempotent and, like Stop, does not cancel or wait for a callback already admitted for execution; it may begin or continue after Close returns.

func (*Watchdog) Reset

func (t *Watchdog) Reset()

Reset arms or restarts a full timeout from now whether the watchdog is dormant, stopped, or expired. It is a no-op on a closed or disabled watchdog.

func (*Watchdog) Start added in v0.2.0

func (t *Watchdog) Start()

Start arms a fresh timeout when the watchdog is inactive. It does not postpone an already armed countdown and is a no-op on a closed or disabled watchdog.

func (*Watchdog) Stop

func (t *Watchdog) Stop()

Stop cancels the current countdown but leaves the watchdog reusable by a later Start or Reset. Stop does not cancel or wait for a callback already admitted for execution; it may begin or continue after Stop returns.

Jump to

Keyboard shortcuts

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