(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] (let [action (atom nil) >action (m/watch action) id (swap! id-counter inc)] (m/ap (reset! action [:add id {:start >start :end >end :metadata >metadata :flow >flow}]) (try (m/?< >action) (catch missionary.Cancelled _ [:remove id]))))) (comment (def cancel ((m/reduce prn nil (time-object 1 2 :a (m/ap))) prn prn)) (cancel)) (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] (apply m/latest vector time-objects)) (comment (def cancel ((m/reduce prn nil (time-object-collection (time-object 1 2 :a (m/ap)) (time-object 3 4 :a (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-colletion]) (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