summaryrefslogtreecommitdiff
path: root/src/unheard/time_object.clj
blob: 3556321c872b5a58907bc2e4662f4f0cd4906992 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
(ns unheard.time-object
  (:require [missionary.core :as m]
            [helins.interval.map :as imap]))

;; DESIGN
;; A "time object" is any object with a lifetime that is temporally bounded. The
;; goal of the time object abstraction is to allow for for efficient,
;; low-latency allocation and deallocation of an unlimited number of time
;; objects.
;;
;;It is important that a time tree can be efficiently queried by both a range
;;(for UI) and a point (for a song).
;;
;; IMPLEMENTATION
;; Time objects are stored in an interval tree. 

;; Requirements
;; - time objects returned by a timeline range query will include metadata like
;;   start time and end time
;; - Flows associated with time objects should only mount or dismount when
;;   the result of the query changes. I think the best way to accomplish this
;;   is to have the query return a flow of time objects, and then have some
;;   separate function responsible for "playing" these time objects.
;; 
;; Question:
;; Should time objects take an interval tree as an argument,
;; or should they return a flow of interval information (start, end, value)
;; that can be fed into some kind of reactive interval map bookkeeper?
;; I think the latter.
;;

(def id-counter (atom 0))

(defn time-object
  "A time-object takes a start time, and end time, and a value.
  Value is a flow that will be consumed when the corresponding
  time tree is consumed at a point in time within the time-object's
  interval."
  [>start >end >metadata >flow]
  (m/ap
   (let [id (swap! id-counter inc)]
     (m/amb=
      [id
       :add
       {:start >start
        :end >end
        :metadata >metadata
        :flow >flow}]
      (try
        (m/? m/never)
        (catch missionary.Cancelled _ [id :remove]))))))

(comment
  (def cancel
    ((m/reduce prn nil (time-object 1 2 :a (m/ap)))
     prn prn))

  (cancel))

(defn with-final
  []
  (m/ap
   (m/amb=
    :start
    (try
      (m/? m/never)
      (catch missionary.Cancelled _ :end)))))

(defn merge-flows [& flows]
  (m/ap
   (m/?<
    (m/?> (count flows) (m/seed flows)))))

(defn time-object-collection
  "Takes a flow of [diff-action time-object-id time-object], where:
  - diff-action is one of either :add or :remove
  - time-object-id is a unique identifier
  - time-object is the time object in question

  Returns a collection of time objects, represented as a flow."
  [& time-objects]
  ;; Goal: use group-by to emit just twice per time object
  (m/ap (m/?>  (try (apply m/zip vector time-objects) (catch missionary.Cancelled _)))))

(comment
  (def cancel
    ((m/reduce prn nil
               (time-object-collection
                (time-object 1 2 :a (m/ap))
                (time-object 3 4 :b (m/ap))
                (time-object 5 6 :c (m/ap)))) prn prn))

  (cancel))

;; m/store is an optimization, allowing diffs to be dropped prior to processing
;; by consumer. Think :add 1, :remove 1

(defn merge-tocs
  "Merge multiple time-object-collections. Returns a new time-object-collection."
  [& time-object-collections]
  (apply merge-flows time-object-collections))

(comment
  (def cancel
    ((m/reduce prn nil
               (merge-tocs
                (time-object-collection
                 (time-object 1 2 :a (m/ap))
                 (time-object 3 4 :b (m/ap))
                 (time-object 5 6 :c (m/ap)))
                (time-object-collection
                 (time-object 1 2 :d (m/ap))
                 (time-object 3 4 :e (m/ap))
                 (time-object 5 6 :f (m/ap))))) prn prn))
  ;; Whoa! Running cancel twice cancels twice...
  ;; https://clojurians.slack.com/archives/CL85MBPEF/p1763154775780589?thread_ts=1763149125.436899&cid=CL85MBPEF
  (cancel))

(defn timeline
  "Primary timeline bookkeeping mehanism."
  [time-object-collection]
  (m/ap))

(defn point-query
  "Query a timeline. Returns a flow of time objects."
  [>timeline >at]
  (m/ap
   (let [[tl at] (m/?< (m/latest vector >timeline >at))]
     (get tl at))))

(defn range-query
  "Range query. Returns a flow of time objects."
  [timeline >range])
(defn run
  "Runs the flows associated with a collection of time objects."
  ;; TODO: "Running" a time object has different meanings for different objects.
  ;; How should I think about that?
  [>query-result])

;; How should a time tree work?
;; Well, we eventually want to end up with a single time tree
;; Is it important to be able to merge time trees together?
;; Part of me thinks "yes"
;; Time objects cannot be composed together - that is, you can't
;; take two time objects and add them together to get a new time object
;; However, you _can_ add two time objects together to get a time tree
;; The alternative to having intermediate time trees would be to have
;; some kind of time-object-collection abstraction.
;; time-object-collection would have a merge function.
;; A time object collection would be responsible for all of the bookkeeping
;; related to the lifetimes of time objects.
;; My suspicion is that this will