|
| 1 | +(defpackage #:io.github.cl-sdk.wst.idempotency |
| 2 | + (:use #:cl) |
| 3 | + (:documentation "Core idempotency-key primitives independent of HTTP objects.") |
| 4 | + (:import-from #:io.github.cl-sdk.wst.idempotency.store |
| 5 | + #:create-entry |
| 6 | + #:update-entry |
| 7 | + #:delete-entry |
| 8 | + #:idempotency-entry-response) |
| 9 | + (:export |
| 10 | + #:cached-response |
| 11 | + #:make-cached-response |
| 12 | + #:cached-response-status |
| 13 | + #:cached-response-headers |
| 14 | + #:cached-response-content |
| 15 | + #:idempotency-engine |
| 16 | + #:make-idempotency-engine |
| 17 | + #:idempotency-engine-ttl-seconds |
| 18 | + #:idempotency-engine-clock |
| 19 | + #:idempotency-engine-cache-response-p |
| 20 | + #:valid-idempotency-key-p |
| 21 | + #:register-request |
| 22 | + #:drop-request |
| 23 | + #:store-response)) |
| 24 | + |
| 25 | +(in-package #:io.github.cl-sdk.wst.idempotency) |
| 26 | + |
| 27 | +(defstruct cached-response |
| 28 | + "Serializable response snapshot for replay." |
| 29 | + status |
| 30 | + headers |
| 31 | + content) |
| 32 | + |
| 33 | +(defclass idempotency-engine () |
| 34 | + ((ttl-seconds :initarg :ttl-seconds |
| 35 | + :initform 86400 |
| 36 | + :reader idempotency-engine-ttl-seconds) |
| 37 | + (clock :initarg :clock |
| 38 | + :initform #'get-universal-time |
| 39 | + :reader idempotency-engine-clock) |
| 40 | + (cache-response-p :initarg :cache-response-p |
| 41 | + :initform (lambda (response) |
| 42 | + (< (cached-response-status response) 500)) |
| 43 | + :reader idempotency-engine-cache-response-p)) |
| 44 | + (:documentation "Core idempotency orchestration object. |
| 45 | +
|
| 46 | +Engine subclasses can define their own storage slots and specialize |
| 47 | +io.github.cl-sdk.wst.idempotency.store:create-entry, |
| 48 | +io.github.cl-sdk.wst.idempotency.store:update-entry and |
| 49 | +io.github.cl-sdk.wst.idempotency.store:delete-entry for storage behavior. |
| 50 | +For a complete engine implementation, specialize all three methods.")) |
| 51 | + |
| 52 | +(defgeneric register-request (engine scope key fingerprint) |
| 53 | + (:documentation "Register lifecycle processing ownership for SCOPE/KEY/FINGERPRINT. |
| 54 | +
|
| 55 | +Returns two values: |
| 56 | +- DECISION: one of :started, :replay, :in-progress, :conflict |
| 57 | +- PAYLOAD: cached-response for :replay, NIL otherwise.")) |
| 58 | + |
| 59 | +(defgeneric store-response (engine scope key fingerprint response) |
| 60 | + (:documentation "Persist RESPONSE for SCOPE/KEY/FINGERPRINT. |
| 61 | +
|
| 62 | +Returns T when completion happened, NIL otherwise.")) |
| 63 | + |
| 64 | +(defgeneric drop-request (engine scope key fingerprint) |
| 65 | + (:documentation "Release processing lock for SCOPE/KEY/FINGERPRINT. |
| 66 | +
|
| 67 | +Returns T when release happened, NIL otherwise.")) |
| 68 | + |
| 69 | +(defun make-idempotency-engine () |
| 70 | + (error "No default store-backed idempotency engine is provided in io.github.cl-sdk.wst.idempotency. Use a concrete engine implementation such as (make-instance 'io.github.cl-sdk.wst.idempotency.memory-store:memory-idempotency-engine) or your own subclass.")) |
| 71 | + |
| 72 | +(defun valid-idempotency-key-p (value) |
| 73 | + "Return T when VALUE is a non-empty key of at most 255 chars. |
| 74 | +
|
| 75 | +Key formatting/normalization is caller-managed." |
| 76 | + (and (stringp value) |
| 77 | + (not (string= "" value)) |
| 78 | + (<= (length value) 255))) |
| 79 | + |
| 80 | +(defmethod register-request ((engine idempotency-engine) scope key fingerprint) |
| 81 | + (multiple-value-bind (status entry) |
| 82 | + (create-entry engine |
| 83 | + (list scope key) |
| 84 | + fingerprint |
| 85 | + (idempotency-engine-ttl-seconds engine) |
| 86 | + (funcall (idempotency-engine-clock engine))) |
| 87 | + (ecase status |
| 88 | + (:started (values :started nil)) |
| 89 | + (:in-progress (values :in-progress nil)) |
| 90 | + (:conflict (values :conflict nil)) |
| 91 | + (:replay (values :replay (idempotency-entry-response entry)))))) |
| 92 | + |
| 93 | +(defmethod store-response ((engine idempotency-engine) scope key fingerprint response) |
| 94 | + (update-entry engine |
| 95 | + (list scope key) |
| 96 | + fingerprint |
| 97 | + response |
| 98 | + (idempotency-engine-ttl-seconds engine) |
| 99 | + (funcall (idempotency-engine-clock engine)))) |
| 100 | + |
| 101 | +(defmethod drop-request ((engine idempotency-engine) scope key fingerprint) |
| 102 | + (delete-entry engine |
| 103 | + (list scope key) |
| 104 | + fingerprint)) |
0 commit comments