/[meteor]/googlecode.com/svn/trunk/public_html/meteor.js
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Annotation of /googlecode.com/svn/trunk/public_html/meteor.js

Parent Directory Parent Directory | Revision Log Revision Log


Revision 62 - (hide annotations)
Thu Nov 27 00:33:21 2008 UTC (15 years, 5 months ago) by andrew.betts
File MIME type: application/javascript
File size: 11088 byte(s)
1 Fixed: Added SIGPIPE handler.  We noticed that under heavy load
Meteor receives SIGPIPEs from the OS, suspected to relate to clients
that have just disconnected the moment Meteor attempts to write to the
socket.  This caused Meteor to crash.
2 Fixed: Long polling multiple channels no longer causes the loop to
die and restart when some channels have messages queued for delivery.
3 Fixed: Over time, Meteor 'collected' connections from clients that
never got disconnected even if MaxTime was set.  This happened if the
client concerned sent a header with no terminating blank line.  Meteor
kept waiting for the rest of the header, which never arrived, and
therefore the client remained in limbo, never subjected to the MaxTime
time limit because it had not yet become a subscriber.  Clients are
now allowed 30 seconds to send a valid request header.
4 Fixed: If only one message existed on the server, the JS client
would continue to request it again and again, because it has message
ID 0, and the JS client considered this an invalid message ID.
5 Fixed: Corrected some comments in file headers

6 Changed: MaxMessages has been renamed to CloseOnEvent and functions
in a similar, but not quite identical way.  Thanks to Matthew Haak,
who pointed out the extreme confusingness of MaxMessages and a bug
that has resulted in Fix 2 above.  Setting CloseOnEvent to any value
that evaluates to true will cause Meteor to close subscriber
connections after at least one message has been sent and there are no
further messages pending.  This is identical to MaxMessages for values
of 0 and 1, but where MaxMessages is set to a value higher than one,
replacing it with CloseOnEvent with the same value will act as though
it were set to one.  The intent of MaxMessages was to enable long-
polling (and it is used by the JS client in that way), and
CloseonEvent is a drop in replacement for that behaviour.
7 Changed: Meteor JS client now uses dynamic <SCRIPT> tags for all
polling behaviours, rather than XHR.  This enables it to make poll
requests cross-domain (see 13)
8 Changed: Meteor JS client now abstracts timestamp lookups to a
dedicated method.
9 Changed: Default HeaderTemplates no longer include cache busting
headers, since all meteor requests contain a millisecond timestamp and
so no client makes the same request twice.  These were therefore
simply chewing up bandwidth.
10 Changed: Date strings used for logging debug messages are cached to
avoid numerous expensive lookups to localtime().
11 Changed: Channel info is only sent in a response if the client does
not request a restart from a specified ID.  The logic being that if
the client knows the ID they want to start from, they have already
made previous requests and have the channel information they need.
Bandwidth saving measure.

12 Added: JS client now has a Meteor.isSupportedBrowser() method,
which you can call to detemine whether Meteor will run in the user's
browser version.
13 Added: JS client can now use different hosts for polling and
streaming.  This is only really useful if your website is on a domain
that has a lot of cookies, and you don't want to send them in every
poll request.  Removing cookies from request headers can reduce the
size of the request significantly.  We find that with cookies included
Meteor poll requests are usually larger than the responses.  To use,
set Meteor.pollhost.  Meteor.pollhost can be any domain, while
Meteor.host must be a subdomain of your website hostname.
14 Added: Config file now supports new 'FooterTemplate' parameter, for
a string to send just before the connection to the subscriber is
closed.  This is in support of change 7.
15 Added: Better inline documentation for ChannelInfoTemplate config
parameter
16 Added: Log output includes connection IDs corresponding to the file
inode for each connection
17 Added: New controller command LISTCONNECTIONS, produces a newline
delimited list of all currently connected clients, and for each one
displaying "ConnectionID IPAddress ClientType [SubscriberID]"
18 Added: New controller command DESCRIBE, takes a ConnectionID as a
parameter, and outputs numerous statistics about that particular
client, including number of messages sent/received, user agent, IP
address, time connected, time remaining until MaxTime etc.
19 Added: New controller comment LISTSUBSCRIBERS, produces a newline
delimited list of all currently connected streaming subscribers, and
for each one displaying "SubscriberID IPAddress Starttime TimeLimit
TimeRemaining MessageCount UserAgent"
20 Added: SHOWSTATS command produces the following additional stats:
connection_count: total current connections, real_subscribers: total
of number of currently connected streaming subscribers plus the number
of unique polling connections seen in the last 60 seconds.
21 Added: STDERR outputs prior to every exit() for debugging purposes
22 Added: The UDP server is now considered stable, and is the best way
of broadcasting messages to lots of Meteor nodes simultaneously and
efficiently. 


1 andrew.betts 32 /*
2     stream: xhrinteractive, iframe, serversent
3     longpoll
4     smartpoll
5     simplepoll
6     */
7    
8     Meteor = {
9    
10     callbacks: {
11     process: function() {},
12     reset: function() {},
13     eof: function() {},
14     statuschanged: function() {},
15     changemode: function() {}
16     },
17     channelcount: 0,
18     channels: {},
19     debugmode: false,
20     frameref: null,
21     host: null,
22     hostid: null,
23     maxpollfreq: 60000,
24     minpollfreq: 2000,
25     mode: "stream",
26     pingtimeout: 20000,
27     pingtimer: null,
28 andrew.betts 39 pollfreq: 3000,
29 andrew.betts 32 port: 80,
30 andrew.betts 62 pollaborted: false,
31     pollhost: null,
32     pollnum: 0,
33 andrew.betts 32 polltimeout: 30000,
34 andrew.betts 62 polltimer: null,
35 andrew.betts 32 recvtimes: [],
36 andrew.betts 62 lastrequest: null,
37 andrew.betts 32 status: 0,
38     updatepollfreqtimer: null,
39    
40 andrew.betts 62 isSupportedBrowser: function() {
41     var v;
42     if (v = navigator.userAgent.match(/compatible\; MSIE\ ([0-9\.]+)\;/i)) {
43     if (parseFloat(v[1]) <= 5.5) return false;
44     } else if (v = navigator.userAgent.match(/Gecko\/([0-9]+)/i)) {
45     if (parseInt(v[1]) <= 20051015) return false;
46     } else if (v = navigator.userAgent.match(/WebKit\/([0-9\.]+)/i)) {
47     if (parseFloat(v[1]) < 400) return false;
48     }
49     return true;
50     },
51    
52 andrew.betts 32 register: function(ifr) {
53     ifr.p = Meteor.process;
54     ifr.r = Meteor.reset;
55     ifr.eof = Meteor.eof;
56 andrew.betts 53 ifr.ch = Meteor.channelInfo;
57 andrew.betts 32 clearTimeout(Meteor.frameloadtimer);
58     Meteor.setstatus(4);
59     Meteor.log("Frame registered");
60     },
61    
62     joinChannel: function(channelname, backtrack) {
63     if (typeof(Meteor.channels[channelname]) != "undefined") throw "Cannot join channel "+channelname+": already subscribed";
64 andrew.betts 59 Meteor.channels[channelname] = {backtrack:backtrack};
65 andrew.betts 32 Meteor.log("Joined channel "+channelname);
66     Meteor.channelcount++;
67 andrew.betts 61 if (Meteor.status != 0 && Meteor.status != 6) Meteor.connect();
68 andrew.betts 32 },
69    
70     leaveChannel: function(channelname) {
71     if (typeof(Meteor.channels[channelname]) == "undefined") throw "Cannot leave channel "+channelname+": not subscribed";
72     delete Meteor.channels[channelname];
73     Meteor.log("Left channel "+channelname);
74     Meteor.channelcount--;
75 andrew.betts 61 if (Meteor.channelcount && Meteor.status != 0 && Meteor.status != 6) Meteor.connect();
76 andrew.betts 59 else Meteor.disconnect();
77 andrew.betts 32 },
78    
79     connect: function() {
80     Meteor.log("Connecting");
81     if (!Meteor.host) throw "Meteor host not specified";
82     if (isNaN(Meteor.port)) throw "Meteor port not specified";
83     if (!Meteor.channelcount) throw "No channels specified";
84     if (Meteor.status) Meteor.disconnect();
85     Meteor.setstatus(1);
86 andrew.betts 62 if (!Meteor.hostid) Meteor.hostid = Meteor.time()+""+Math.floor(Math.random()*1000000)
87 andrew.betts 32 document.domain = Meteor.extract_xss_domain(document.domain);
88     if (Meteor.mode=="stream") Meteor.mode = Meteor.selectStreamTransport();
89     Meteor.log("Selected "+Meteor.mode+" transport");
90     if (Meteor.mode=="xhrinteractive" || Meteor.mode=="iframe" || Meteor.mode=="serversent") {
91     if (Meteor.mode == "iframe") {
92     Meteor.loadFrame(Meteor.getSubsUrl());
93     } else {
94     Meteor.loadFrame("http://"+Meteor.host+((Meteor.port==80)?"":":"+Meteor.port)+"/stream.html");
95     }
96     clearTimeout(Meteor.pingtimer);
97     Meteor.pingtimer = setTimeout(Meteor.pollmode, Meteor.pingtimeout);
98    
99     } else {
100 andrew.betts 62 Meteor.recvtimes[0] = Meteor.time();
101 andrew.betts 32 if (Meteor.updatepollfreqtimer) clearTimeout(Meteor.updatepollfreqtimer);
102 andrew.betts 60 if (Meteor.mode=='smartpoll') Meteor.updatepollfreqtimer = setInterval(Meteor.updatepollfreq, 10000);
103 andrew.betts 32 if (Meteor.mode=='longpoll') Meteor.pollfreq = Meteor.minpollfreq;
104 andrew.betts 62 Meteor.poll();
105 andrew.betts 32 }
106     },
107    
108     disconnect: function() {
109     if (Meteor.status) {
110     clearTimeout(Meteor.pingtimer);
111     clearTimeout(Meteor.updatepollfreqtimer);
112     clearTimeout(Meteor.frameloadtimer);
113     if (typeof CollectGarbage == 'function') CollectGarbage();
114 andrew.betts 55 if (Meteor.status != 6) Meteor.setstatus(0);
115     Meteor.log("Disconnected");
116 andrew.betts 61 try { Meteor.frameref.parentNode.removeChild(Meteor.frameref); delete Meteor.frameref; return true; } catch(e) { }
117     try { Meteor.frameref.open(); Meteor.frameref.close(); return true; } catch(e) {}
118 andrew.betts 32 }
119     },
120    
121     selectStreamTransport: function() {
122     try {
123     var test = ActiveXObject;
124     return "iframe";
125     } catch (e) {}
126     if ((typeof window.addEventStream) == "function") return "iframe";
127     return "xhrinteractive";
128     },
129    
130     getSubsUrl: function() {
131 andrew.betts 62 var host = ((Meteor.mode=='simplepoll' || Meteor.mode=='smartpoll' || Meteor.mode=='longpoll') && Meteor.pollhost) ? Meteor.pollhost : Meteor.host;
132     var surl = "http://" + host + ((Meteor.port==80)?"":":"+Meteor.port) + "/push/" + Meteor.hostid + "/" + Meteor.mode;
133 andrew.betts 32 for (var c in Meteor.channels) {
134     surl += "/"+c;
135 andrew.betts 62 if (typeof Meteor.channels[c].lastmsgreceived != 'undefined') {
136 andrew.betts 32 surl += ".r"+(Meteor.channels[c].lastmsgreceived+1);
137     } else if (Meteor.channels[c].backtrack > 0) {
138     surl += ".b"+Meteor.channels[c].backtrack;
139 andrew.betts 59 } else if (Meteor.channels[c].backtrack != undefined) {
140 andrew.betts 32 surl += ".h";
141     }
142     }
143 andrew.betts 62 surl += "?nc="+Meteor.time();
144 andrew.betts 32 return surl;
145     },
146    
147     loadFrame: function(url) {
148     try {
149 andrew.betts 39 if (!Meteor.frameref) {
150     var transferDoc = new ActiveXObject("htmlfile");
151     Meteor.frameref = transferDoc;
152     }
153     Meteor.frameref.open();
154     Meteor.frameref.write("<html><script>");
155     Meteor.frameref.write("document.domain=\""+(document.domain)+"\";");
156     Meteor.frameref.write("</"+"script></html>");
157     Meteor.frameref.parentWindow.Meteor = Meteor;
158     Meteor.frameref.close();
159     var ifrDiv = Meteor.frameref.createElement("div");
160     Meteor.frameref.appendChild(ifrDiv);
161 andrew.betts 32 ifrDiv.innerHTML = "<iframe src=\""+url+"\"></iframe>";
162     } catch (e) {
163 andrew.betts 39 if (!Meteor.frameref) {
164     var ifr = document.createElement("IFRAME");
165     ifr.style.width = "10px";
166     ifr.style.height = "10px";
167     ifr.style.border = "none";
168     ifr.style.position = "absolute";
169     ifr.style.top = "-10px";
170     ifr.style.marginTop = "-10px";
171     ifr.style.zIndex = "-20";
172     ifr.Meteor = Meteor;
173     document.body.appendChild(ifr);
174     Meteor.frameref = ifr;
175     }
176     Meteor.frameref.setAttribute("src", url);
177 andrew.betts 32 }
178     Meteor.log("Loading URL '"+url+"' into frame...");
179     Meteor.frameloadtimer = setTimeout(Meteor.frameloadtimeout, 5000);
180     },
181    
182     pollmode: function() {
183     Meteor.log("Ping timeout");
184 andrew.betts 60 if (Meteor.mode != "smartpoll") {
185     Meteor.mode="smartpoll";
186     Meteor.callbacks["changemode"]("poll");
187     clearTimeout(Meteor.pingtimer);
188     Meteor.lastpingtime = false;
189     }
190 andrew.betts 53 Meteor.connect();
191 andrew.betts 32 },
192    
193     process: function(id, channel, data) {
194     if (id == -1) {
195     Meteor.log("Ping");
196     Meteor.ping();
197 andrew.betts 53 } else if (typeof(Meteor.channels[channel]) != "undefined") {
198 andrew.betts 32 Meteor.log("Message "+id+" received on channel "+channel+" (last id on channel: "+Meteor.channels[channel].lastmsgreceived+")\n"+data);
199     Meteor.callbacks["process"](data);
200     Meteor.channels[channel].lastmsgreceived = id;
201     if (Meteor.mode=="smartpoll") {
202 andrew.betts 62 Meteor.recvtimes[Meteor.recvtimes.length] = Meteor.time();
203 andrew.betts 32 while (Meteor.recvtimes.length > 5) Meteor.recvtimes.shift();
204     }
205     }
206     Meteor.setstatus(5);
207     },
208    
209     ping: function() {
210     if (Meteor.pingtimer) {
211     clearTimeout(Meteor.pingtimer);
212     Meteor.pingtimer = setTimeout(Meteor.pollmode, Meteor.pingtimeout);
213 andrew.betts 62 Meteor.lastpingtime = Meteor.time();
214 andrew.betts 32 }
215     Meteor.setstatus(5);
216     },
217    
218     reset: function() {
219 andrew.betts 61 if (Meteor.status != 6 && Meteor.status != 0) {
220 andrew.betts 55 Meteor.log("Stream reset");
221     Meteor.ping();
222     Meteor.callbacks["reset"]();
223 andrew.betts 62 var x = Meteor.pollfreq - (Meteor.time()-Meteor.lastrequest);
224 andrew.betts 55 if (x < 10) x = 10;
225     setTimeout(Meteor.connect, x);
226     }
227 andrew.betts 32 },
228    
229     eof: function() {
230 andrew.betts 55 Meteor.log("Received end of stream, will not reconnect");
231 andrew.betts 32 Meteor.callbacks["eof"]();
232 andrew.betts 55 Meteor.setstatus(6);
233 andrew.betts 53 Meteor.disconnect();
234 andrew.betts 32 },
235    
236 andrew.betts 53 channelInfo: function(channel, id) {
237     Meteor.channels[channel].lastmsgreceived = id;
238     Meteor.log("Received channel info for channel "+channel+": resume from "+id);
239     },
240    
241 andrew.betts 32 updatepollfreq: function() {
242     var avg = 0;
243     for (var i=1; i<Meteor.recvtimes.length; i++) {
244     avg += (Meteor.recvtimes[i]-Meteor.recvtimes[i-1]);
245     }
246 andrew.betts 62 avg += (Meteor.time()-Meteor.recvtimes[Meteor.recvtimes.length-1]);
247 andrew.betts 32 avg /= Meteor.recvtimes.length;
248     var target = avg/2;
249     if (target < Meteor.pollfreq && Meteor.pollfreq > Meteor.minpollfreq) Meteor.pollfreq = Math.ceil(Meteor.pollfreq*0.9);
250     if (target > Meteor.pollfreq && Meteor.pollfreq < Meteor.maxpollfreq) Meteor.pollfreq = Math.floor(Meteor.pollfreq*1.05);
251     },
252    
253     registerEventCallback: function(evt, funcRef) {
254     Function.prototype.andThen=function(g) {
255     var f=this;
256     var a=Meteor.arguments
257     return function(args) {
258     f(a);g(args);
259     }
260     };
261     if (typeof Meteor.callbacks[evt] == "function") {
262     Meteor.callbacks[evt] = (Meteor.callbacks[evt]).andThen(funcRef);
263     } else {
264     Meteor.callbacks[evt] = funcRef;
265     }
266     },
267    
268     frameloadtimeout: function() {
269     Meteor.log("Frame load timeout");
270     if (Meteor.frameloadtimer) clearTimeout(Meteor.frameloadtimer);
271     Meteor.setstatus(3);
272 andrew.betts 53 Meteor.pollmode();
273 andrew.betts 32 },
274    
275     extract_xss_domain: function(old_domain) {
276     if (old_domain.match(/^(\d{1,3}\.){3}\d{1,3}$/)) return old_domain;
277     domain_pieces = old_domain.split('.');
278     return domain_pieces.slice(-2, domain_pieces.length).join(".");
279     },
280    
281     setstatus: function(newstatus) {
282     // Statuses: 0 = Uninitialised,
283     // 1 = Loading stream,
284     // 2 = Loading controller frame,
285     // 3 = Controller frame timeout, retrying.
286     // 4 = Controller frame loaded and ready
287     // 5 = Receiving data
288 andrew.betts 55 // 6 = End of stream, will not reconnect
289 andrew.betts 32
290     if (Meteor.status != newstatus) {
291     Meteor.status = newstatus;
292     Meteor.callbacks["statuschanged"](newstatus);
293     }
294     },
295    
296     log: function(logstr) {
297     if (Meteor.debugmode) {
298     if (window.console) {
299     window.console.log(logstr);
300     } else if (document.getElementById("meteorlogoutput")) {
301     document.getElementById("meteorlogoutput").innerHTML += logstr+"<br/>";
302     }
303     }
304 andrew.betts 62 },
305    
306     poll: function() {
307     Meteor.pollaborted = 0;
308     try {
309     clearTimeout(Meteor.polltimer);
310     } catch (e) {};
311     Meteor.lastrequest = Meteor.time();
312     if (Meteor.polltimeout) Meteor.polltimer = setTimeout(Meteor.clearpoll, Meteor.polltimeout);
313     var scripttag = document.createElement("SCRIPT");
314     scripttag.type = "text/javascript";
315     scripttag.src = Meteor.getSubsUrl();
316     scripttag.id = "meteorpoll"+(++Meteor.pollnum);
317     scripttag.className = "meteorpoll";
318     document.getElementsByTagName("HEAD")[0].appendChild(scripttag);
319     },
320    
321     clearpoll: function() {
322     var s = document.getElementById('meteorpoll'+Meteor.pollnum);
323     if (typeof s != 'undefined') s.parentNode.removeChild(s);
324     if (Meteor.status) {
325     var x = parent.Meteor.pollfreq - (Meteor.time()-Meteor.lastrequest);
326     if (x < 10) x = 10;
327     setTimeout(Meteor.poll, x);
328     }
329     },
330    
331     time: function() {
332     var now = new Date();
333     return now.getTime();
334 andrew.betts 32 }
335     }
336    
337     var oldonunload = window.onunload;
338     if (typeof window.onunload != 'function') {
339     window.onunload = Meteor.disconnect;
340     } else {
341     window.onunload = function() {
342     if (oldonunload) oldonunload();
343     Meteor.disconnect();
344     }
345     }

  ViewVC Help
Powered by ViewVC 1.1.26