package api
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"sync"
"craftdesign.group/quarry/internal/ops"
)
// IdempotencyStore remembers the first answer to a keyed request
// (oltp/idempotency.go).
type IdempotencyStore interface {
IdemRecall(scope, key string) (digest, result string, ok bool, err error)
IdemRemember(scope, key, digest, result string) error
}
// idemLocks serializes one (scope, key) at a time, so two copies of the
// same retry arriving together file one ticket, not two.
var idemLocks sync.Map
// idempotent runs a filing op at most once per key. A caller that sends
// the same Idempotency-Key again — an agent retrying after a dropped
// connection, a form posted twice — gets the first answer back instead
// of a second ticket. The key is scoped to the op, the repository and
// the caller, so one caller's key never reaches another's answer; the
// same key with a DIFFERENT request is refused rather than guessed at,
// because answering it with the first ticket would hide that the second
// was never filed. No key, or a forge with no store, runs as before.
func idempotent[In any, Out any](ctx context.Context, d Deps, op, repo, key string, in In, run func() (Out, error)) (Out, error) {
var zero Out
if key == "" || d.Idem == nil {
return run()
}
scope := op + "|" + repo + "|" + author(ctx)
mu, _ := idemLocks.LoadOrStore(scope+"|"+key, &sync.Mutex{})
mu.(*sync.Mutex).Lock()
defer mu.(*sync.Mutex).Unlock()
raw, _ := json.Marshal(in)
sum := sha256.Sum256(raw)
digest := hex.EncodeToString(sum[:])
was, result, ok, err := d.Idem.IdemRecall(scope, key)
if err != nil {
return zero, err
}
if ok {
if was != digest {
return zero, fmt.Errorf("this Idempotency-Key already filed a different request; use a new key for a new ticket: %w", ops.ErrInvalid)
}
var out Out
if err := json.Unmarshal([]byte(result), &out); err != nil {
return zero, err
}
return out, nil
}
out, err := run()
if err != nil {
return out, err
}
if b, merr := json.Marshal(out); merr == nil {
if rerr := d.Idem.IdemRemember(scope, key, digest, string(b)); rerr != nil {
log.Printf("idempotency: %s: remembering %s: %v", scope, key, rerr)
}
}
return out, nil
}