-
Notifications
You must be signed in to change notification settings - Fork 0
/
chat.edh
116 lines (98 loc) · 4.46 KB
/
chat.edh
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
# this ctor bootstraps the chat world
class RunCtrl {
method __init__ (accessPoint as this.accessPoint) pass
chatters = {,} # the dict of all chatters by name
method dismiss () { # this is called on Ctrl^C at the console
runtime.info <| 'Dismissing all active chatters'
ai { # use a tx, don't get a racing joining chatter lost/leaked
committee = this.chatters
this.chatters = {,}
}
for (_name, chatter) from committee do if chatter!=nil then
chatter.kicked <- 'server purge'
}
# this ctor allocates a private namespace (object) for a new user session
class Chat {
kicked = sink # create a new sink for kicked-out event
reactor kicked reason {
runtime.debug <| 'Done with ' ++name++ ' - ' ++reason
# posting a nil:<str> pair as the msg out, to trigger `cutoffHuman`
# mechanism defined for the agency model
out <- nil: if reason != nil
then "You have been kicked: " ++ reason
else ''
break # `break` from a reactor breaks the thread, 'Chat' then stops
}
method __init__ (
incoming as this.incoming, out as this.out,
) {
this.name = '<stranger>' # to appear in the log on premature disconnection
out <- "What is your name?"
for name from incoming do case type(name) of {
StringType -> {
this.name = name
# use a tx to mediate naming contentions
ai case chatters[name] of nil -> {
chatters[name] = this
break # to break the for-from-do loop
}
out <- "The name " ++name++ " is in use, please choose another"
# not doing fallthrough here, the loop will continue
}
# abnormal input for name, this chatter is destined to be kicked out
kicked <- case name of {
{ cmd:_ } -> case cmd of {
'quit' -> nil # human left without answering the name
fallthrough # other malicious cmds
}
runtime.warn <| 'Some one tried to use name: ' ++ name
'misbehaving, adversarial name - ' ++ name
}
}
defer { # defered code is guaranteed to run on thread termination
runtime.debug <| 'Cleaning up for ' ++ name
# need a tx to not cleanup a later live chatter with same name
ai if chatters[name] == this then chatters[name] = nil
}
# here is a tiny window for user msgs through `incoming` get dropped,
# that between the subscription loop below and the name prompting loop
# above. this is not a big deal for our chat business, left as a demo.
for msg from incoming do case msg of {
# the mre (most-recent-event) from `incoming` atm tends to be the
# user's answer to name inquiry, it's very probably be the very first
# `msg` seen here, we take this chance to greet the user.
# well this handling may surprise the user when later he/she typed
# his/her own name, intending to send as a broadcast message.
name -> out <- ' 🎉 Welcome ' ++ name ++ '!'
{ cmd:args } -> case cmd of {
'kick' -> case args of { { (who,) } -> case chatters[who] of {
nil -> out <- who ++ ' is not connected'
{ chatter } -> {
chatter.kicked <- 'by ' ++ this.name
out <- 'you kicked ' ++ who
}
} out <- 'Invalid args to /kick: ' ++ args }
'tell' -> case args of { { (who, what) } -> case chatters[who] of {
nil -> out <- who ++ ' is not connected'
{ chatter } -> chatter.out <- '*'++name++'*: ' ++ what
} out <- 'Invalid args to /tell: ' ++ args }
'quit' -> { out <- nil:'Bye!'; kicked <- nil }
out <- 'Unrecognised command: ' ++ msg
}
# run to here means none of the branches above matched, so it's a public
# message and let's broadcast it
# it's okay to use a snapshot of all live chatters, no tx here
for (_name, chatter) from chatters do if chatter!=nil then
chatter.out <- '<'++ this.name ++'>: ' ++ msg
}
}
}
method run () { # this is the method to keep the world running
# each time a new agent enters the chat world, a pair of sinks for its
# incoming and outgoing messages are posted through the sink of access point
for (in, out) from accessPoint do {
go Chat(in, out) # start a chatter thread to do the IO
in=nil out=nil # unref so they're garbage-collectable after chatter left
}
}
}