-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgetting_started.html
More file actions
519 lines (452 loc) · 19.7 KB
/
Copy pathgetting_started.html
File metadata and controls
519 lines (452 loc) · 19.7 KB
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
---
layout: default
title: Getting Started
---
# Server
For these first examples, we shall write no server code and just use a
plain AtomizeJS server. See the [install][] guide for acquiring the
libraries. Once you've done that, you just need to put the following
boiler-plate in a file (call it `app.js`):
{% highlight javascript %}
var http = require('http');
var atomize = require('atomize-server');
var port = 9999;
var httpServer = http.createServer();
atomize.create(httpServer, '[/]atomize');
console.log(" [*] Listening on 0.0.0.0:" + port);
httpServer.listen(port, '0.0.0.0');
{% endhighlight %}
Then start up NodeJS with
node app.js
As mentioned in the [install][] guide, if you wish to use experimental
features of JavaScript, and have prepared a version of NodeJS
correctly, then you'll instead start up NodeJS as
node --harmony-collections --harmony-proxies app.js
This will create a server listening on port 9999 that will provide
access to the AtomizeJS system under the path `/atomize`.
# Client
In the following examples, when we wish to output something, we use
`console.log` which will mean you'll need to be watching the
JavaScript console in your web browser. Obviously, you can rework
these to achieve fancy interactions with the DOM if you so wish.
All these examples assume the following HTML template:
{% highlight html %}
<!DOCTYPE html>
<html>
<head>
<title>AtomizeJS Example</title>
<script type="text/javascript" src="http://cdn.sockjs.org/sockjs-0.1.min.js"></script>
<script type="text/javascript" src="https://raw.github.com/atomizejs/cereal/master/lib/cereal.js"></script>
<script type="text/javascript" src="https://raw.github.com/atomizejs/atomize-client/master/lib/compat.js"></script>
<script type="text/javascript" src="https://raw.github.com/atomizejs/atomize-client/master/lib/atomize.js"></script>
<script type="text/javascript" src="atomize-example.js"></script>
</head>
<body onload="start();">
</body>
</html>
{% endhighlight %}
We will then write the rest of the example in the `atomize-example.js`
file.
None of these examples need to be *served* by a web-server: provided
you have the AtomizeJS server running as described above, pointing
your browser directly at the HTML files on your machine (i.e. with a
`file:///...` URL) will work perfectly well.
# Compatibility
In most cases, you're probably not going to be using a really
bleeding-edge web-browser which supports experimental JavaScript
features. In order for AtomizeJS to work with normal web-browsers,
there's a [translation tool][translate] provided. After doing an `npm
install atomize-client`, you should find `npm` has installed an
`atomize-translate` binary.
You should then write the examples in an `atomize-example.js.raw`
file, and translate them by running:
atomize-translate atomize-example.js.raw atomize-example.js atomize
# Writing and reading
{% highlight javascript %}
function start() {
var atomize = new Atomize("http://localhost:9999/atomize");
atomize.onAuthenticated = function () {
atomize.atomically(function () {
if (atomize.root.x === undefined) {
atomize.root.x = (new Date()).toString();
return "Wrote " + atomize.root.x;
} else {
var result = atomize.root.x;
delete atomize.root.x;
return "Read " + result;
}
}, function (result) {
console.log(result);
});
};
atomize.connect();
}
{% endhighlight %}
This introduces quite a few concepts.
1. We create a new Atomize object, and tell it that it's going to
connect to the AtomizeJS server on localhost, port 9999. If this is
not the case, then please adjust this as required.
2. We set up a function to run once we've managed to connect and
authenticate with the server. We're not going to cover
authentication at this stage, but the hooks are there to implement
any form of authentication you want. As the server code above is
written, there are no authentication steps to perform, so we can
assume that the `onAuthenticated` function is going to be run as
soon as we've connected to the server.
3. The [`atomize.atomically`][`atomically`] function takes two
arguments.
1. The first argument is the function that is run as a
transaction, atomically. This function may run more than once:
if it is found to have read or written data which has in the
mean time been modified by another transaction, then the effects
of this function will be undone, and the transaction is
restarted automatically. Any result that this function returns
is passed to the second argument.
2. The second argument is a continuation function. It is run once
the transaction successfully commits, and is passed any result
returned from the transaction function. This function will only
be run at most once.
4. [`atomize.root`][`root`] is the root of the distributed object
graph. Assigning values to properties and objects beneath the
[`atomize.root`][`root`] object will ensure that those values are
distributed to all clients using this AtomizeJS server. The initial
value of the [`atomize.root`][`root`] object is the empty object,
`{}`.
5. The same transaction has different behaviours depending on what it
finds. If the client is the first client to run the transaction,
then it will find that the `x` property is `undefined`. It will
then assign to the `x` property, and return, thus completing the
transaction. The second client will find that the `x` property is
defined, and so will go into the other branch of the `if`
statement.
6. State continues to exist in the server even if clients leave. If
you start the AtomizeJS server, then point a client at this HTML
page, then that client will be the first client to run the
transaction, and will write the current date to the `x` property of
the [`root`][] object. The effect of this transaction exists on the
server. Thus even if you shut your browser down and restart it,
when you next visit the page, you'll be the second client there,
and thus will read the previously written date.
7. Having set up the function to run once we're authenticated, we then
call `connect` to set it all in motion.
## The importance of continuations
It is easy to forget that the continuation-passing-style model is
required. For example, you might be tempted to write the above code
as:
{% highlight javascript %}
function start() {
var result,
atomize = new Atomize("http://localhost:9999/atomize");
atomize.onAuthenticated = function () {
atomize.atomically(function () {
if (atomize.root.x === undefined) {
atomize.root.x = (new Date()).toString();
result = "Wrote " + atomize.root.x;
} else {
var result = atomize.root.x;
delete atomize.root.x;
result = "Read " + result;
}
});
};
atomize.connect();
console.log(result);
}
{% endhighlight %}
If you do this, what you'll most likely see is that every browser that
visits the HTML page will log to its console `Wrote ...` and the
date. This is because whilst the first client really will write the
date to the `x` property, the second client will start the transaction
with the default empty root object, and thus start down the first
branch of the `if` statement. It is only when the second client tries
to commit the effects of the transaction that it'll find out from the
AtomizeJS server that it ran the transaction function on out-of-date
data. At that point, the second client will be sent the most
up-to-date version of the [`root`][] object, which will have the `x`
property set, and the transaction function will be restarted.
But the communication from the client to the AtomizeJS server is
asynchronous, which means that the transaction function will return
and execution will continue to the `console.log` line whilst the
transaction is trying to commit for the first time (which will likely
fail). Thus even though the transaction function will be rerun, and
even though eventually the `result` variable will be set to the
correct value, the `console.log` line will have been run too early,
and the wrong result output because at that point in time, the
transaction function had gone down the first branch, and thus assigned
the `Wrote ...` result to the `result` variable. This is why it's
important to use the continuation-passing-style whenever you need to
depend on the transaction having committed successfully.
## Avoiding side effects
Equally, it's important to avoid doing things within a transaction
which have *side-effects*. For example, if you rewrote this code as:
{% highlight javascript %}
function start() {
var atomize = new Atomize("http://localhost:9999/atomize");
atomize.onAuthenticated = function () {
atomize.atomically(function () {
if (atomize.root.x === undefined) {
atomize.root.x = (new Date()).toString();
console.log("Wrote " + atomize.root.x);
} else {
var result = atomize.root.x;
delete atomize.root.x;
console.log("Read " + result);
}
});
};
atomize.connect();
}
{% endhighlight %}
then on the second client to visit the page, you'll probably see two
lines output on the console: the first being `Wrote ...` and the
second being `Read ...`. The reason is the same as described above
(i.e. the second client has to rerun the transaction function as the
commit will fail after the first run), but now, because we're doing
the `console.log` *within* the transaction function, the `console.log`
will occur every time the transaction function is run.
Whilst a transaction that fails to commit undoes all the modifications
of objects managed by AtomizeJS (including everything that is or has
been reachable from [`atomize.root`][`root`]), it can't undo other
actions. Thus modifications of the DOM and other operations that cause
side-effects should only be done from the continuation to the
transaction function. Thus it's best practise to return values from
the transaction that allow the continuation to figure out what it
should be doing. The continuation is only run once, and only run once
the transaction has successfully committed, so by that point, the
result of the transaction function is stable.
## Writing Objects and Primitives
So far, we have written only primitive values into the [`root`][]
object graph. When you assign a primitive value (i.e. a boolean, a
string, a number or `undefined`), you don't need to do anything
special. However, if you assign an object, then you need to [`lift`][]
the object to ensure that AtomizeJS starts managing the object
correctly. For example:
{% highlight javascript %}
function start() {
var atomize = new Atomize("http://localhost:9999/atomize");
atomize.onAuthenticated = function () {
atomize.atomically(function () {
if (atomize.root.x === undefined) {
atomize.root.x = atomize.lift({a: "hello"});
atomize.root.x.date = (new Date()).toString();
return "Wrote " + atomize.root.x.date;
} else {
var result = atomize.root.x.date;
delete atomize.root.x;
return "Read " + result;
}
}, function (result) {
console.log(result);
});
};
atomize.connect();
}
{% endhighlight %}
If you do not call [`lift`][] then you'll get an error. Please note
that currently, if you [`lift`][] in an object that has a non-default
`prototype` then the prototype does not get recreated by other
clients: the other clients will just see an object with a prototype of
`Object.prototype`. Additionally, only plain `Objects` and `Arrays`
are supported: trying to lift a `Function` will not work.
## Objects outside of [`root`][]
Any call to [`lift`][] will return an object that is managed by
AtomizeJS. This need not be part of the [`root`][] object. For
example:
{% highlight javascript %}
var myObj;
function start() {
var atomize = new Atomize("http://localhost:9999/atomize");
atomize.onAuthenticated = function () {
atomize.atomically(function () {
return atomize.lift({});
}, function (obj) {
myObj = obj;
});
};
atomize.connect();
}
{% endhighlight %}
`myObj` now contains an object that is managed by AtomizeJS. Of
course, because it's not reachable from the [`root`][] object, it
won't get distributed to other clients. But, for example, we can take
objects that were reachable from the [`root`][] object, and hold on to
them, even if later on, they get detached from the [`root`][]
object. We can use this to build a broadcast queue...
# Building communication
So far, we've seen how you can build transactions that act on what
they find in the [`root`][] object, and can modify properties and
values within the [`root`][] object. For communication between
different parties, you need *sending* operations and *receiving*
operations. In our case, *sending* is very simple: it's just writing
to a known location within the [`root`][] object. For *receiving*,
we'd like to avoid *spinning*: if no new value has been written for us
to receive, we don't want to be constantly restarting the
transaction. Instead, we want the AtomizeJS system to remember the
transaction, and automatically restart it when the values of variables
we've read so far in the transaction have been changed. This avoids
the need for spinning. This is the purpose of the [`retry`][]
operation. For example:
{% highlight javascript %}
var atomize = new Atomize("http://localhost:9999/atomize");
function start() {
atomize.onAuthenticated = function () {
if (Math.random() > 0.2) {
receive();
} else {
send();
}
};
atomize.connect();
}
function receive() {
atomize.atomically(function () {
if (atomize.root.value === undefined) {
atomize.retry();
} else {
return "Received " + atomize.root.value;
}
}, function (result) {
console.log(result);
});
}
function send() {
atomize.atomically(function () {
atomize.root.value = (new Date()).toString();
});
}
{% endhighlight %}
Try opening up several browser windows and pointing them all at the
same HTML page.
Here, we have an 80% chance that a new client will enter the `receive`
function. If there is no value to receive then it will be suspended,
and only woken up and restarted once the variables that it has read so
far (in this case, just the [`root`][] object) are modified.
As you start more browsers, eventually one of them will enter the
`send` function, and will write to the `value` property of the
[`root`][] object. At this point, all the other clients, waiting in
the `receive` transaction, will have their transaction function
restarted. The second time through this receive transaction, the
clients will see the newly written `value` property. This time,
they'll not go into the [`retry`][] branch of the `if` statement, and
instead will receive the result and complete the transaction. We've
just built a broadcast mechanism.
Whilst the [`retry`][] operation suspends the current transaction, it
too does not block (i.e. the client's thread of execution will leave
the outer-most [`atomically`][] invocation and continue with
subsequent statements). Thus as before, it's important to use
continuation-passing-style when you need to run code after the
transaction has finally completed. Note that no statements that
directly follow a [`retry`][] will ever be invoked: they are dead
code.
## A broadcast queue
This example "puts it all together". It is a broadcast queue with one
writer which constantly adds new values to the end of the queue. Every
reader will receive every value that was added *after* the client
connected. All clients can work at their own pace.
This example makes use of [`retry`][] and the fact that you can take
objects out of the [`root`][] object graph and they're still managed
by AtomizeJS: this is how each client maintains its own position into
the queue.
The queue that we construct is going to be a standard
linked-list. Each *cell* has a property `value` (containing the value
of the cell), and a property `next` (which points to the next
cell). The `root.queue` pointer is always going to be the *tail* of
the queue, where new values get added.
First, we need to ensure we only have one writer:
{% highlight javascript %}
var atomize = new Atomize("http://localhost:9999/atomize");
var writer;
var nextValue = 0;
function start() {
atomize.onAuthenticated = function () {
atomize.atomically(function () {
if (atomize.root.queue === undefined) {
atomize.root.queue = atomize.lift({
next: atomize.lift({}),
value: nextValue
});
return true;
} else {
return false;
}
}, function (w) {
writer = w;
loop();
});
};
atomize.connect();
}
{% endhighlight %}
By the end of this, the variable `writer` will contain `true` for just
one client -- the client that arrived first. Now we need to set up the
loop. We want to make sure this doesn't block the browser so we don't
use a `while` loop or recursion. Instead, we use `setTimeout`:
{% highlight javascript %}
function loop() {
if (writer) {
setTimeout("write();", 1);
} else {
setTimeout("read();", 1);
}
}
{% endhighlight %}
Now to write to the queue:
{% highlight javascript %}
function write() {
nextValue += 1;
atomize.atomically(function () {
var cell = atomize.root.queue.next;
cell.next = atomize.lift({});
cell.value = nextValue;
atomize.root.queue = cell;
return cell.value;
}, function (value) {
console.log("Wrote " + value);
loop();
});
}
{% endhighlight %}
The *tail* of the queue will always contain a pointer to the `next`
cell which will be empty. Thus we take the `next` cell, and populate
it with our new value (and new empty cell), and then assign that back
to the `queue`, thus adding to the tail of the queue. Once the `write`
transaction has completed, we loop back around to write the next
value.
Now for reading:
{% highlight javascript %}
var myPosition;
function read() {
if (myPosition === undefined) {
atomize.atomically(function () {
if (myPosition === undefined) {
myPosition = atomize.root.queue;
}
}, function () {
read();
});
} else {
atomize.atomically(function () {
if (myPosition.value === undefined) {
atomize.retry();
} else {
return {value: myPosition.value, next: myPosition.next};
}
}, function (result) {
myPosition = result.next;
console.log("Read " + result.value);
loop();
});
}
}
{% endhighlight %}
First, we have to populate the `myPosition` variable with the cell at
the current tail of the queue, which we do in a separate
transaction. Once we have a cell in `myPosition` we wait for it to
have a `value` property, and once it does, we grab that value, and
then update our position to the next cell in the linked list. This
way, we don't rely on keeping up with the writer. The value of
`myPosition` is an object that is managed by AtomizeJS: it was created
by a call to [`lift`][] in `write`. If the writer goes faster than the
reader then the value of `myPosition` will differ from
`atomize.root.queue`, but that's ok: the reader will always be able to
follow the links and walk the chain to the tail of the queue.