mirror of
https://github.com/imjasonh/webpush
synced 2026-07-12 01:59:54 +00:00
Implement Web Push API Go library with VAPID support and pluggable storage
Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com>
This commit is contained in:
parent
7d0082c045
commit
78bd40dc88
15 changed files with 2708 additions and 1 deletions
168
storage/memory.go
Normal file
168
storage/memory.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/imjasonh/webpush"
|
||||
)
|
||||
|
||||
// ErrNotFound is returned when a record is not found.
|
||||
var ErrNotFound = errors.New("record not found")
|
||||
|
||||
// Memory implements in-memory storage for testing and development.
|
||||
type Memory struct {
|
||||
mu sync.RWMutex
|
||||
records map[string]*Record
|
||||
}
|
||||
|
||||
// NewMemory creates a new in-memory storage.
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{
|
||||
records: make(map[string]*Record),
|
||||
}
|
||||
}
|
||||
|
||||
// Save stores or updates a subscription.
|
||||
func (m *Memory) Save(_ context.Context, record *Record) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if record.CreatedAt.IsZero() {
|
||||
record.CreatedAt = now
|
||||
}
|
||||
record.UpdatedAt = now
|
||||
|
||||
// Make a copy to avoid external mutations
|
||||
stored := &Record{
|
||||
ID: record.ID,
|
||||
UserID: record.UserID,
|
||||
CreatedAt: record.CreatedAt,
|
||||
UpdatedAt: record.UpdatedAt,
|
||||
Subscription: &webpush.Subscription{
|
||||
Endpoint: record.Subscription.Endpoint,
|
||||
Keys: webpush.Keys{
|
||||
P256dh: record.Subscription.Keys.P256dh,
|
||||
Auth: record.Subscription.Keys.Auth,
|
||||
},
|
||||
},
|
||||
}
|
||||
m.records[record.ID] = stored
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a subscription by ID.
|
||||
func (m *Memory) Get(_ context.Context, id string) (*Record, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
record, ok := m.records[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return copyRecord(record), nil
|
||||
}
|
||||
|
||||
// GetByEndpoint retrieves a subscription by its endpoint URL.
|
||||
func (m *Memory) GetByEndpoint(_ context.Context, endpoint string) (*Record, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, record := range m.records {
|
||||
if record.Subscription.Endpoint == endpoint {
|
||||
return copyRecord(record), nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// GetByUserID retrieves all subscriptions for a user.
|
||||
func (m *Memory) GetByUserID(_ context.Context, userID string) ([]*Record, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var results []*Record
|
||||
for _, record := range m.records {
|
||||
if record.UserID == userID {
|
||||
results = append(results, copyRecord(record))
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Delete removes a subscription by ID.
|
||||
func (m *Memory) Delete(_ context.Context, id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, ok := m.records[id]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(m.records, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByEndpoint removes a subscription by its endpoint URL.
|
||||
func (m *Memory) DeleteByEndpoint(_ context.Context, endpoint string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for id, record := range m.records {
|
||||
if record.Subscription.Endpoint == endpoint {
|
||||
delete(m.records, id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
// List returns all subscriptions with pagination.
|
||||
func (m *Memory) List(_ context.Context, limit, offset int) ([]*Record, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Collect all records
|
||||
var all []*Record
|
||||
for _, record := range m.records {
|
||||
all = append(all, record)
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
if offset >= len(all) {
|
||||
return nil, nil
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(all) {
|
||||
end = len(all)
|
||||
}
|
||||
|
||||
results := make([]*Record, 0, end-offset)
|
||||
for i := offset; i < end; i++ {
|
||||
results = append(results, copyRecord(all[i]))
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Close is a no-op for in-memory storage.
|
||||
func (m *Memory) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyRecord(r *Record) *Record {
|
||||
return &Record{
|
||||
ID: r.ID,
|
||||
UserID: r.UserID,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
Subscription: &webpush.Subscription{
|
||||
Endpoint: r.Subscription.Endpoint,
|
||||
Keys: webpush.Keys{
|
||||
P256dh: r.Subscription.Keys.P256dh,
|
||||
Auth: r.Subscription.Keys.Auth,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue