transaction.ss
(module transaction mzscheme
  
  (require (lib "contract.ss")
           (lib "cut.ss" "srfi" "26"))
  
  (require (planet "list.ss" ("untyped" "unlib.plt" 2))
           (planet "parameter.ss" ("untyped" "unlib.plt" 2)))
  
  (require (file "base.ss")
           (file "era-private.ss"))
  
  ; Here's the idea:
  ;
  ; - Every time you start a transaction, you create a frame in which you record
  ;   deltas to persistent-structs.
  ;
  ; - There is a parameter called current-frame that points to the frame you are
  ;   currently using to store roll-back information.
  ;
  ; - When you start a sub-transaction, you create a "child" frame of the current
  ;   frame, and set that to be the current-frame.
  ;
  ; - Rolling back a given transaction involves rolling back its frame and any
  ;   child frames that haven't already been rolled back.
  ;
  ; - Even if a sub-transaction completes successfully, its parent still keeps a
  ;   link to its frame. If the parent is rolled back, it can roll back the child.
  ;
  ; That's about it.
  
  ; Structure types ------------------------------
  
  ;; struct delta : (alist-of symbol any)
  ;;
  ;; Records changes to a structure in case they are rolled back.
  ;; As the struct is mutated, its old field values are recorded
  ;; in the fields attribute of a delta.
  ;;
  ;; Changes to the struct can be rolled back by calling the
  ;; roll-back! procedure and passing in the original struct and the
  ;; delta.
  (define-struct delta (fields) #f)
  
  ;; struct frame : (hash-table-of persistent-struct delta) (list-of frame) boolean
  ;;
  ;; The "deltas" field maps persistent structs to the old values
  ;; of their fields.
  (define-struct frame (deltas children rolled-back?) #f)
  
  ;; current-frame : (parameter (U frame #f))
  (define current-frame (make-parameter #f (make-guard frame? "frame")))
  
  ;; roll-back-persistent-structs? : (parameter boolean)
  (define roll-back-persistent-structs? 
    (make-parameter 
     #t
     (lambda (value)
       (cond [(not (boolean? value))
              (raise-exn exn:fail:contract
                (format "Expected boolean, received: ~a" value))]
             [(current-frame)
              (raise-exn exn:fail:snooze:transaction
                "Cannot set value of roll-back-persistent-structs?: transaction is in progress.")]
             [else value]))))
  
  ; Constructors ---------------------------------
  
  ;; create-frame : -> frame
  (define (create-frame)
    (make-frame (make-hash-table) null #f))
  
  ; Accessors / mutators -------------------------
  
  ;; set-delta-field! : delta symbol any -> void
  (define (set-delta-field! delta field old-value)
    (unless (assoc field (delta-fields delta))
      (set-delta-fields! 
       delta
       (cons (cons field old-value) 
             (delta-fields delta)))))
  
  ;; frame-delta : frame struct -> delta
  ;;
  ;; Retrieves the delta stored for the specified struct. If no
  ;; matching delta is present, a new delta is created and stored.
  (define (frame-delta frame struct)
    (let ([deltas (frame-deltas frame)])
      (hash-table-get
       deltas 
       struct
       ; Failure thunk
       (lambda ()
         (define delta (make-delta null))
         (hash-table-put! deltas struct delta)
         delta))))
  
  ; Rolling back ---------------------------------
  
  ;; roll-back-delta! : persistent-struct delta -> void
  ;;
  ;; Rolls back all changes stored in the delta, including changes to
  ;; data attributes and changes to the revision of the struct.
  (define (roll-back-delta! struct delta)
    (let* ([fields        (delta-fields delta)]
           [revision      (assoc-value/default 'revision fields #f)])
      (when revision
        (set-revision!/internal struct revision))
      (set-attributes/alist! struct fields)))
    
  ; Public interface -----------------------------

  ;; in-transaction : -> boolean
  (define (in-transaction?)
    (if (current-frame) #t #f))
  
  ;; call-with-frame : frame (-> any) -> any
  ;;
  ;; Takes a new transaction frame as an argument, installs it as a child
  ;; of the current frame (if any), resets the current-frame parameter appropriately
  ;; and calls proc.
  (define (call-with-frame frame proc)
    (let ([parent (current-frame)])
      (when parent
        (set-frame-children! parent (cons frame (frame-children parent))))
      (parameterize ([current-frame frame])
        (proc))))
  
  ;; record-delta! : persistent-struct symbol any -> void
  (define (record-delta! struct field old-value)
    (let ([frame (current-frame)])
      (when frame
        (set-delta-field! (frame-delta frame struct) field old-value))))
  
  ;; roll-back-frame! : frame -> void
  ;;
  ;; Rolls back all deltas stored in the specified frame.
  (define (roll-back-frame! frame)
    (let ([children (frame-children frame)]
          [deltas   (frame-deltas frame)])
      ; Roll back the current frame:
      (hash-table-for-each
       (frame-deltas frame)
       (lambda (struct delta)
         (roll-back-delta! struct delta)))
      ; Mark it as rolled back so we don't try it again:
      (set-frame-rolled-back?! frame #t)
      ; Roll back child, if it exists and hasn't already been rolled back:
      (for-each (lambda (child)
                  (when (not (frame-rolled-back? child))
                    (roll-back-frame! child)))
                children)))
    
  ; Provide statements ---------------------------
  
  (provide roll-back-persistent-structs?)
  
  (provide/contract
   [rename create-frame make-frame (-> frame?)]
   [call-with-frame                (-> frame? procedure? any/c)]
   [in-transaction?                (-> boolean?)]
   [record-delta!                  (-> persistent-struct? symbol? any/c void?)]
   [roll-back-frame!               (-> frame? void?)])
  
  )