Git fork
1# Simple Chord for Tcl
2#
3# A "chord" is a method with more than one entrypoint and only one body, such
4# that the body runs only once all the entrypoints have been called by
5# different asynchronous tasks. In this implementation, the chord is defined
6# dynamically for each invocation. A SimpleChord object is created, supplying
7# body script to be run when the chord is completed, and then one or more notes
8# are added to the chord. Each note can be called like a proc, and returns
9# immediately if the chord isn't yet complete. When the last remaining note is
10# called, the body runs before the note returns.
11#
12# The SimpleChord class has a constructor that takes the body script, and a
13# method add_note that returns a note object. Since the body script does not
14# run in the context of the procedure that defined it, a mechanism is provided
15# for injecting variables into the chord for use by the body script. The
16# activation of a note is idempotent; multiple calls have the same effect as
17# a simple call.
18#
19# If you are invoking asynchronous operations with chord notes as completion
20# callbacks, and there is a possibility that earlier operations could complete
21# before later ones are started, it is a good practice to create a "common"
22# note on the chord that prevents it from being complete until you're certain
23# you've added all the notes you need.
24#
25# Example:
26#
27# # Turn off the UI while running a couple of async operations.
28# lock_ui
29#
30# set chord [SimpleChord::new {
31# unlock_ui
32# # Note: $notice here is not referenced in the calling scope
33# if {$notice} { info_popup $notice }
34# }
35#
36# # Configure a note to keep the chord from completing until
37# # all operations have been initiated.
38# set common_note [$chord add_note]
39#
40# # Activate notes in 'after' callbacks to other operations
41# set newnote [$chord add_note]
42# async_operation $args [list $newnote activate]
43#
44# # Communicate with the chord body
45# if {$condition} {
46# # This sets $notice in the same context that the chord body runs in.
47# $chord eval { set notice "Something interesting" }
48# }
49#
50# # Activate the common note, making the chord eligible to complete
51# $common_note activate
52#
53# At this point, the chord will complete at some unknown point in the future.
54# The common note might have been the first note activated, or the async
55# operations might have completed synchronously and the common note is the
56# last one, completing the chord before this code finishes, or anything in
57# between. The purpose of the chord is to not have to worry about the order.
58
59# SimpleChord class:
60# Represents a procedure that conceptually has multiple entrypoints that must
61# all be called before the procedure executes. Each entrypoint is called a
62# "note". The chord is only "completed" when all the notes are "activated".
63class SimpleChord {
64 field notes
65 field body
66 field is_completed
67 field eval_ns
68
69 # Constructor:
70 # set chord [SimpleChord::new {body}]
71 # Creates a new chord object with the specified body script. The
72 # body script is evaluated at most once, when a note is activated
73 # and the chord has no other non-activated notes.
74 constructor new {i_body} {
75 set notes [list]
76 set body $i_body
77 set is_completed 0
78 set eval_ns "[namespace qualifiers $this]::eval"
79 return $this
80 }
81
82 # Method:
83 # $chord eval {script}
84 # Runs the specified script in the same context (namespace) in which
85 # the chord body will be evaluated. This can be used to set variable
86 # values for the chord body to use.
87 method eval {script} {
88 namespace eval $eval_ns $script
89 }
90
91 # Method:
92 # set note [$chord add_note]
93 # Adds a new note to the chord, an instance of ChordNote. Raises an
94 # error if the chord is already completed, otherwise the chord is
95 # updated so that the new note must also be activated before the
96 # body is evaluated.
97 method add_note {} {
98 if {$is_completed} { error "Cannot add a note to a completed chord" }
99
100 set note [ChordNote::new $this]
101
102 lappend notes $note
103
104 return $note
105 }
106
107 # This method is for internal use only and is intentionally undocumented.
108 method notify_note_activation {} {
109 if {!$is_completed} {
110 foreach note $notes {
111 if {![$note is_activated]} { return }
112 }
113
114 set is_completed 1
115
116 namespace eval $eval_ns $body
117 delete_this
118 }
119 }
120}
121
122# ChordNote class:
123# Represents a note within a chord, providing a way to activate it. When the
124# final note of the chord is activated (this can be any note in the chord,
125# with all other notes already previously activated in any order), the chord's
126# body is evaluated.
127class ChordNote {
128 field chord
129 field is_activated
130
131 # Constructor:
132 # Instances of ChordNote are created internally by calling add_note on
133 # SimpleChord objects.
134 constructor new {c} {
135 set chord $c
136 set is_activated 0
137 return $this
138 }
139
140 # Method:
141 # [$note is_activated]
142 # Returns true if this note has already been activated.
143 method is_activated {} {
144 return $is_activated
145 }
146
147 # Method:
148 # $note activate
149 # Activates the note, if it has not already been activated, and
150 # completes the chord if there are no other notes awaiting
151 # activation. Subsequent calls will have no further effect.
152 method activate {} {
153 if {!$is_activated} {
154 set is_activated 1
155 $chord notify_note_activation
156 }
157 }
158}