/[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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 62 - (show annotations)
Thu Nov 27 00:33:21 2008 UTC (15 years, 4 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 /*
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 pollfreq: 3000,
29 port: 80,
30 pollaborted: false,
31 pollhost: null,
32 pollnum: 0,
33 polltimeout: 30000,
34 polltimer: null,
35 recvtimes: [],
36 lastrequest: null,
37 status: 0,
38 updatepollfreqtimer: null,
39
40 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 register: function(ifr) {
53 ifr.p = Meteor.process;
54 ifr.r = Meteor.reset;
55 ifr.eof = Meteor.eof;
56 ifr.ch = Meteor.channelInfo;
57 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 Meteor.channels[channelname] = {backtrack:backtrack};
65 Meteor.log("Joined channel "+channelname);
66 Meteor.channelcount++;
67 if (Meteor.status != 0 && Meteor.status != 6) Meteor.connect();
68 },
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 if (Meteor.channelcount && Meteor.status != 0 && Meteor.status != 6) Meteor.connect();
76 else Meteor.disconnect();
77 },
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 if (!Meteor.hostid) Meteor.hostid = Meteor.time()+""+Math.floor(Math.random()*1000000)
87 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 Meteor.recvtimes[0] = Meteor.time();
101 if (Meteor.updatepollfreqtimer) clearTimeout(Meteor.updatepollfreqtimer);
102 if (Meteor.mode=='smartpoll') Meteor.updatepollfreqtimer = setInterval(Meteor.updatepollfreq, 10000);
103 if (Meteor.mode=='longpoll') Meteor.pollfreq = Meteor.minpollfreq;
104 Meteor.poll();
105 }
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 if (Meteor.status != 6) Meteor.setstatus(0);
115 Meteor.log("Disconnected");
116 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 }
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 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 for (var c in Meteor.channels) {
134 surl += "/"+c;
135 if (typeof Meteor.channels[c].lastmsgreceived != 'undefined') {
136 surl += ".r"+(Meteor.channels[c].lastmsgreceived+1);
137 } else if (Meteor.channels[c].backtrack > 0) {
138 surl += ".b"+Meteor.channels[c].backtrack;
139 } else if (Meteor.channels[c].backtrack != undefined) {
140 surl += ".h";
141 }
142 }
143 surl += "?nc="+Meteor.time();
144 return surl;
145 },
146
147 loadFrame: function(url) {
148 try {
149 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 ifrDiv.innerHTML = "<iframe src=\""+url+"\"></iframe>";
162 } catch (e) {
163 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 }
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 if (Meteor.mode != "smartpoll") {
185 Meteor.mode="smartpoll";
186 Meteor.callbacks["changemode"]("poll");
187 clearTimeout(Meteor.pingtimer);
188 Meteor.lastpingtime = false;
189 }
190 Meteor.connect();
191 },
192
193 process: function(id, channel, data) {
194 if (id == -1) {
195 Meteor.log("Ping");
196 Meteor.ping();
197 } else if (typeof(Meteor.channels[channel]) != "undefined") {
198 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 Meteor.recvtimes[Meteor.recvtimes.length] = Meteor.time();
203 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 Meteor.lastpingtime = Meteor.time();
214 }
215 Meteor.setstatus(5);
216 },
217
218 reset: function() {
219 if (Meteor.status != 6 && Meteor.status != 0) {
220 Meteor.log("Stream reset");
221 Meteor.ping();
222 Meteor.callbacks["reset"]();
223 var x = Meteor.pollfreq - (Meteor.time()-Meteor.lastrequest);
224 if (x < 10) x = 10;
225 setTimeout(Meteor.connect, x);
226 }
227 },
228
229 eof: function() {
230 Meteor.log("Received end of stream, will not reconnect");
231 Meteor.callbacks["eof"]();
232 Meteor.setstatus(6);
233 Meteor.disconnect();
234 },
235
236 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 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 avg += (Meteor.time()-Meteor.recvtimes[Meteor.recvtimes.length-1]);
247 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 Meteor.pollmode();
273 },
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 // 6 = End of stream, will not reconnect
289
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 },
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 }
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