Comparing version 0.1.2 to 0.1.3
@@ -5,3 +5,3 @@ var R = require('ramda'); | ||
// Let's create a stream of numbers | ||
var numbers = flyd.stream(0); | ||
var numbers = flyd.stream(); | ||
@@ -11,3 +11,4 @@ var isEven = R.compose(R.eq(0), R.modulo(R.__, 2)); | ||
// All even numbers multiplied by 3 | ||
var evenTimes3 = flyd.transduce(R.pipe( | ||
var drop3evenTimes3 = flyd.transduce(R.compose( | ||
R.drop(3), | ||
R.filter(isEven), | ||
@@ -17,4 +18,4 @@ R.map(R.multiply(3)) | ||
flyd.map(function(n) { console.log('evenTimes3: ' + n); }, evenTimes3); | ||
flyd.map(function(n) { console.log(n); }, drop3evenTimes3); | ||
numbers(9)(4)(3)(7)(6)(5); | ||
numbers(9)(4)(2)(8)(6)(5)(2); |
149
flyd.js
@@ -17,15 +17,13 @@ (function (root, factory) { | ||
function isUndefined(v) { | ||
return v === undefined; | ||
function notUndef(v) { | ||
return v !== undefined; | ||
} | ||
function each(fn, list) { | ||
for (var i = 0; i < list.length; ++i) fn(list[i]); | ||
} | ||
var toUpdate = []; | ||
var inStream; | ||
function removeListener(listeners, s) { | ||
var idx = listeners.indexOf(s); | ||
listeners[idx] = listeners[listeners.length - 1]; | ||
listeners.length--; | ||
} | ||
function map(s, f) { | ||
@@ -44,7 +42,7 @@ return stream([s], function() { return f(s()); }); | ||
var merge = curryN(2, function(s1, s2) { | ||
var s = stream([s1, s2], function(n, changed) { | ||
var s = immediate(stream([s1, s2], function(n, changed) { | ||
return changed[0] ? changed[0]() | ||
: s1.hasVal ? s1() | ||
: s2(); | ||
}, true); | ||
})); | ||
endsOn(stream([s1.end, s2.end], function(self, changed) { | ||
@@ -61,6 +59,2 @@ return true; | ||
function of(v) { | ||
return stream(v); | ||
} | ||
function initialDepsNotMet(stream) { | ||
@@ -76,6 +70,6 @@ if (!stream.depsMet) { | ||
function updateStream(s) { | ||
if (initialDepsNotMet(s) || s.ended) return; | ||
if (initialDepsNotMet(s) || (s.end && s.end())) return; | ||
inStream = s; | ||
var returnVal = s.fn(s, s.depsChanged); | ||
if (returnVal !== undefined) { | ||
if (notUndef(returnVal)) { | ||
s(returnVal); | ||
@@ -90,5 +84,3 @@ } | ||
s.queued = true; | ||
for (var i = 0; i < s.listeners.length; ++i) { | ||
findDeps(order, s.listeners[i]); | ||
} | ||
each(findDeps.bind(null, order), s.listeners); | ||
order.push(s); | ||
@@ -100,10 +92,6 @@ } | ||
var i, order = []; | ||
for (i = 0; i < s.listeners.length; ++i) { | ||
if (s.listeners[i].end === s) { | ||
end(s.listeners[i]); | ||
} else { | ||
s.listeners[i].depsChanged.push(s); | ||
findDeps(order, s.listeners[i]); | ||
} | ||
} | ||
each(function(list) { | ||
list.end === s ? endStream(list) | ||
: (list.depsChanged.push(s), findDeps(order, list)); | ||
}, s.listeners); | ||
for (i = order.length - 1; i >= 0; --i) { | ||
@@ -118,22 +106,5 @@ if (order[i].depsChanged.length > 0) { | ||
function flushUpdate() { | ||
for (var s; s = toUpdate.shift();) { | ||
updateDeps(s); | ||
} | ||
while (toUpdate.length > 0) updateDeps(toUpdate.shift()); | ||
} | ||
function end(s) { | ||
s.ended = true; | ||
if (s.deps) s.deps.forEach(function(dep) { removeListener(dep.listeners, s); }); | ||
} | ||
function endsOn(endS, s) { | ||
if (s.end) { | ||
removeListener(s.end.listeners, s); | ||
if (isUndefined(s.end.end)) end(s.end); | ||
} | ||
s.end = endS; | ||
endS.listeners.push(s); | ||
return s; | ||
} | ||
function isStream(stream) { | ||
@@ -160,9 +131,5 @@ return isFunction(stream) && 'hasVal' in stream; | ||
} else { | ||
for (var j = 0; j < s.listeners.length; ++j) { | ||
if (s.listeners[j].end === s) { | ||
end(s.listeners[j]); | ||
} else { | ||
s.listeners[j].depsChanged.push(s); | ||
} | ||
} | ||
each(function(list) { | ||
list.end === s ? endStream(list) : list.depsChanged.push(s); | ||
}, s.listeners); | ||
} | ||
@@ -179,7 +146,6 @@ return s; | ||
s.end = undefined; | ||
s.ended = false; | ||
s.map = map.bind(null, s); | ||
s.ap = ap; | ||
s.of = of; | ||
s.of = stream; | ||
s.toString = streamToString; | ||
@@ -190,21 +156,53 @@ | ||
function createDependentStream(deps, fn, dontWaitForDeps) { | ||
function createDependentStream(deps, fn) { | ||
var s = createStream(); | ||
s.fn = fn; | ||
s.deps = deps; | ||
s.depsMet = dontWaitForDeps; | ||
s.depsMet = false; | ||
s.depsChanged = []; | ||
deps.forEach(function(dep) { | ||
dep.listeners.push(s); | ||
}); | ||
each(function(dep) { dep.listeners.push(s); }, deps); | ||
return s; | ||
} | ||
function stream(arg, fn, dontWaitForDeps) { | ||
function immediate(s) { | ||
if (s.depsMet === false) { | ||
s.depsMet = true; | ||
updateStream(s); | ||
flushUpdate(); | ||
} | ||
return s; | ||
} | ||
function removeListener(s, listeners) { | ||
var idx = listeners.indexOf(s); | ||
listeners[idx] = listeners[listeners.length - 1]; | ||
listeners.length--; | ||
} | ||
function detachDeps(s) { | ||
each(function(dep) { removeListener(s, dep.listeners); }, s.deps); | ||
s.deps.length = 0; | ||
} | ||
function endStream(s) { | ||
if (s.deps) detachDeps(s); | ||
if (s.end) detachDeps(s.end); | ||
} | ||
function endsOn(endS, s) { | ||
detachDeps(s.end); | ||
endS.listeners.push(s.end); | ||
s.end.deps.push(endS); | ||
return s; | ||
} | ||
function stream(arg, fn) { | ||
var s, deps; | ||
var endStream = createDependentStream([], function() { return true; }); | ||
if (arguments.length > 1) { | ||
deps = arg.filter(function(d) { return d !== undefined; }); | ||
s = createDependentStream(deps, fn, isUndefined(dontWaitForDeps) ? false : true); | ||
var depEndStreams = deps.filter(function(d) { return !isUndefined(d.end); }) | ||
.map(function(d) { return d.end; }); | ||
deps = arg.filter(notUndef); | ||
s = createDependentStream(deps, fn); | ||
s.end = endStream; | ||
endStream.listeners.push(s); | ||
var depEndStreams = deps.map(function(d) { return d.end; }).filter(notUndef); | ||
endsOn(createDependentStream(depEndStreams, function() { return true; }, true), s); | ||
@@ -215,3 +213,4 @@ updateStream(s); | ||
s = createStream(); | ||
endsOn(createStream(), s); | ||
s.end = endStream; | ||
endStream.listeners.push(s); | ||
if (arguments.length === 1) s(arg); | ||
@@ -223,12 +222,23 @@ } | ||
var transduce = curryN(2, function(xform, source) { | ||
xform = xform(new StreamTransformer(stream)); | ||
return stream([source], function() { | ||
return xform.step(undefined, source()); | ||
xform = xform(new StreamTransformer()); | ||
// Latest Ramda release still uses old transducer protocol | ||
var stepName = xform['@@transducer/step'] ? '@@transducer/step' : 'step'; | ||
return stream([source], function(self) { | ||
var res = xform[stepName](undefined, source()); | ||
if (res && res['@@transducer/reduced'] === true) { | ||
self.end(true); | ||
return res['@@transducer/value']; | ||
} else { | ||
return res; | ||
} | ||
}); | ||
}); | ||
function StreamTransformer(res) { } | ||
function StreamTransformer() { } | ||
StreamTransformer.prototype.init = function() { }; | ||
StreamTransformer.prototype.result = function() { }; | ||
StreamTransformer.prototype.step = function(s, v) { return v; }; | ||
StreamTransformer.prototype['@@transducer/init'] = function() { }; | ||
StreamTransformer.prototype['@@transducer/result'] = function() { }; | ||
StreamTransformer.prototype['@@transducer/step'] = function(s, v) { return v; }; | ||
@@ -357,4 +367,5 @@ // Own curry implementation snatched from Ramda | ||
_: _, | ||
immediate: immediate, | ||
}; | ||
})); |
{ | ||
"name": "flyd", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"description": "The less is more, modular, functional reactive programming library", | ||
@@ -17,3 +17,3 @@ "main": "flyd.js", | ||
"ramda": "^0.13.0", | ||
"transducers.js": "^0.2.3" | ||
"transducers.js": "0.3.x" | ||
}, | ||
@@ -20,0 +20,0 @@ "scripts": { |
269
README.md
@@ -56,3 +56,3 @@ [![Build Status](https://travis-ci.org/paldepind/flyd.svg?branch=master)](https://travis-ci.org/paldepind/flyd) | ||
This is not general tutorial to functional reactive programming. For that take | ||
This is not general introduction to functional reactive programming. For that take | ||
a look at [The introduction to Reactive Programming you've been | ||
@@ -63,11 +63,18 @@ missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) and/or [this Elm | ||
This tutorial will introduce you to the core of Flyd and show how to use it to | ||
build FRP abstractions. | ||
This is not a demonstration of how you would write code with Flyd on a day to | ||
day basis. For that take a look at the [examples](#examples). | ||
This tutorial will however introduce you to the minimal but powerful core that | ||
Flyd provides and show you how it can be used to build FRP abstractions. | ||
### Creating streams | ||
Flyd gives you streams as the building block for creating reactive dataflows. | ||
The function `stream` creates a representation of a value that changes over time. | ||
A stream is a function. At first sight it works a bit like a getter-setter: | ||
They serve the same purpose as what other FRP libraries call Signals, Observables, | ||
Properties and EventEmitters. | ||
The function `flyd.stream` creates a representation of a value that changes | ||
over time. The resulting stream is a function. At first sight it works a bit | ||
like a getter-setter: | ||
```javascript | ||
@@ -104,4 +111,4 @@ // Create a stream with initial value 5. | ||
as in the above examples we can pass it a list of dependencies and a function. | ||
The function should produce a value based on its dependencies. This new value | ||
results in a new stream. | ||
The function should produce a value based on its dependencies. This new | ||
returned value results in a new stream. | ||
@@ -111,3 +118,3 @@ Flyd automatically updates the stream whenever a dependency changes. This | ||
changes. You can think of dependent stream as streams that automatically | ||
listens/subscribes to their dependencies. | ||
listens to or subscribes to their dependencies. | ||
@@ -147,4 +154,5 @@ ```javascript | ||
The body of a dependent stream is called with two streams: itself and the last | ||
changed stream on which it depends. | ||
The body of a dependent stream is called with two parameters: itself and a list | ||
of the dependencies that has changed since its last invocation (due to [atomic | ||
updates](#atomic-updates) several streams could have changed). | ||
@@ -158,5 +166,6 @@ ```javascript | ||
console.log('Last sum was ' + sum()); | ||
if (changed) { // On the initial call no stream has changed | ||
var changedName = (changed === y ? 'y' : 'x'); | ||
console.log(changedName + ' changed to ' + changed()); | ||
// On the initial call no streams has changed and `changed` will be [] | ||
changed.map(function(s) { | ||
var changedName = (s === y ? 'y' : 'x'); | ||
console.log(changedName + ' changed to ' + s()); | ||
} | ||
@@ -186,3 +195,4 @@ return x() + y(); | ||
`undefined`). Fortunately a streams body will not be called before all of its declared | ||
streams has recieved a value. | ||
streams has recieved a value (this behaviour can be cirumvented with | ||
[flyd.immediate](#flydimmediatestream)). | ||
@@ -208,6 +218,6 @@ ### Using promises for asynchronous operations | ||
You've now seen the basic building block which Flyd provides. Let's see what we | ||
can do with it. Lets write a function that takes a stream and a function and | ||
returns a new stream with the function applied to every value emitted by the | ||
stream. In short, a `map` function. | ||
You've now seen most of the basic building block which Flyd provides. Let's see | ||
what we can do with them. Lets write a function that takes a stream and a | ||
function and returns a new stream with the function applied to every value | ||
emitted by the stream. In short, a `map` function. | ||
@@ -226,5 +236,5 @@ ```javascript | ||
Flyd includes a similar map function as part of its core. | ||
Flyd includes a similar `map` function as part of its core. | ||
### Reducing a stream | ||
### Scaning a stream | ||
@@ -243,8 +253,57 @@ Lets try something else: a scan function for accumulating a stream! It could | ||
Our scan function takes a accumulator function, in initial value and a stream. | ||
Every time the original stream emit a value we pass it to the accumulator along | ||
with the accumulated value. | ||
Our scan function takes an accumulator function, in initial value and a stream. | ||
Every time the original stream emit a value we pass it to the accumulator | ||
function along with the accumulated value. | ||
Flyd includes a scan function as part of its core. | ||
Flyd includes a `scan` function as part of its core. | ||
### Stream endings | ||
When you create a stream with `flyd.stream` it will have an `end` property | ||
which is also a stream. That is an _end stream_: | ||
```javascript | ||
var s = flyd.stream(); | ||
console.log(flyd.isStream(s.end)); // logs `true` | ||
``` | ||
You can end a stream by pushing `true` into its end stream: | ||
```javascript | ||
var s = flyd.stream(); | ||
s.end(true); // this ends `s` | ||
``` | ||
When you create a dependent stream it's end stream will initially depend on all | ||
the ends streams of its dependencies: | ||
```javascript | ||
var n1 = flyd.stream(); | ||
var n2 = flyd.stream(); | ||
var sum = flyd.stream([n1, n2], function() { | ||
return n1() + n2(); | ||
}); | ||
``` | ||
`sum.end` now depends on `n1.end` and `n2.end`. This means that whenever one of | ||
the `sum`s dependencies end `sum` will end as well. | ||
You can change what a streams end stream depends on with `flyd.endsOn`: | ||
```javascript | ||
var n1 = flyd.stream(); | ||
var n2 = flyd.stream(); | ||
var killer = flyd.stream(); | ||
var sum = flyd.endsOn([n1.end, n2.end, killer], flyd.stream([n1, n2], function() { | ||
return n1() + n2(); | ||
})); | ||
``` | ||
Now `sum` will end if either `n1` ends, `n2` ends or if `killer` emits a value. | ||
The fact that a streams ending is itself a stream is a very powerful concept. | ||
It means that we can use the full expresivenes of Flyd to control when a stream | ||
ends. For an example, take a look at the implementation of | ||
[`takeUntil`](https://github.com/paldepind/flyd-takeuntil). | ||
### Fin | ||
@@ -257,17 +316,94 @@ | ||
### flyd.stream(dependencies, body[, doesNotRequireDeps]) | ||
### flyd.stream() | ||
Creates a new stream. | ||
Creates a new top level stream. | ||
__Signature__ | ||
* `dependencies` (array) – The streams on which this stream depends. | ||
* `body` (function) – The function body of the stream. | ||
* \[`doesNotRequireDeps`\] (boolean) – If `true` the function body can be | ||
invoked even before all dependencies have a value. | ||
`a -> Stream a` | ||
__Returns__ | ||
__Example__ | ||
```javascript | ||
var n = stream(1); // Stream with initial value `1` | ||
var s = stream(); // Stream with no initial value | ||
``` | ||
The created stream. | ||
### flyd.stream(dependencies, body) | ||
Creates a new dependent stream. | ||
__Signature__ | ||
`[Stream *] -> (Stream b -> [Stream *] -> b) -> Stream b` | ||
__Example__ | ||
```javascript | ||
var n1 = flyd.stream(0); | ||
var n2 = flyd.stream(0); | ||
var max = flyd.stream([n1, n2], function(self, changed) { | ||
return n1() > n2() ? n1() : n2(); | ||
}); | ||
``` | ||
###flyd.isStream(stream) | ||
Returns `true` if the supplied argument is a Flyd stream and `false` otherwise. | ||
__Signature__ | ||
`* -> Boolean` | ||
__Example__ | ||
```javascript | ||
var s = flyd.stream(1); | ||
var n = 1; | ||
flyd.isStream(s); //=> true | ||
flyd.isStream(n); //=> false | ||
``` | ||
###flyd.immediate(stream) | ||
By default the body of a dependent stream is only called when all the streams | ||
upon which it depends has a value. `immediate` can circumvent this behaviour. | ||
It immediately invokes the body of a dependent stream. | ||
__Signature__ | ||
`Stream a -> Stream a` | ||
__Example__ | ||
```javascript | ||
var s = flyd.stream(); | ||
var hasItems = flyd.immediate(flyd.stream([s], function() { | ||
return s() !== undefined && s().length > 0; | ||
}); | ||
console.log(hasItems()); // logs `false`. Had `immediate` not been | ||
// used `hasItems()` would've returned `undefined` | ||
s([1]); | ||
console.log(hasItems()); // logs `true`. | ||
s([]); | ||
console.log(hasItems()); // logs `false`. | ||
``` | ||
###flyd.endsOn(endStream, s) | ||
Changes which `endsStream` should trigger the ending of `s`. | ||
__Signature__ | ||
`Stream a -> Stream b -> Stream b` | ||
__Example__ | ||
```javascript | ||
var n = flyd.stream(1); | ||
var killer = flyd.stream(); | ||
// `double` ends when `n` ends or when `killer` emits any value | ||
var double = flyd.endsOn(flyd.merge(n.end, killer), flyd.stream([n], function() { | ||
return 2 * n(); | ||
}); | ||
``` | ||
###flyd.map(fn, s) | ||
@@ -348,21 +484,2 @@ | ||
###flyd.destroy(stream) | ||
If the stream has no dependencies this will detach it from any streams it | ||
depends on. This makes it available for garbage collection if there are no | ||
additional references to it. | ||
__Signature__ | ||
`Stream -> undefined` | ||
__Example__ | ||
```javascript | ||
var s = flyd.map(function() { /* something */ }, someStream); | ||
flyd.destroy(s); | ||
s = undefined; | ||
// `s` can be garbage collected | ||
``` | ||
###flyd.curryN(n, fn) | ||
@@ -385,19 +502,2 @@ | ||
###flyd.isStream(stream) | ||
Returns `true` if the supplied argument is a Flyd stream and `false` otherwise. | ||
__Signature__ | ||
`* -> Boolean` | ||
__Example__ | ||
```javascript | ||
var s = flyd.stream(1); | ||
var n = 1; | ||
flyd.isStream(s); //=> true | ||
flyd.isStream(n); //=> false | ||
``` | ||
###stream() | ||
@@ -433,2 +533,7 @@ | ||
###stream.end | ||
A stream that emits `true` when the stream ends. If `true` is pushed down the | ||
stream the parent stream ends. | ||
###stream.map(f) | ||
@@ -504,2 +609,3 @@ | ||
* [flyd-scanmerge](https://github.com/paldepind/flyd-scanmerge) – Merge and scan several streams into one. | ||
* [flyd-takeuntil](https://github.com/paldepind/flyd-takeuntil) – Emit values from a stream until a second stream emits a value. | ||
* Time related | ||
@@ -513,3 +619,3 @@ * [flyd-aftersilence](https://github.com/paldepind/flyd-aftersilence) – Buffers values from a source stream in an array and emits it after a specified duration of silience from the source stream. | ||
Consider code like the following | ||
Consider the following example: | ||
@@ -525,9 +631,26 @@ ```javascript | ||
The dependency graph looks like this. | ||
``` | ||
a | ||
/ \ | ||
b c | ||
\ / | ||
d | ||
``` | ||
Now, when a value flows down `a`, both `b` and `c` will change because they | ||
depend on `a`. If you merely consider streams as being events you'd expect `d` | ||
depend on `a`. If you merely consider streams as being event emitters you'd expect `d` | ||
to be updated twice. Because `a` triggers `b` triggers `d` after which `a` also | ||
twiggers `c` which again triggers `d`. But Flyd will handle this better. | ||
Since only one value entered the system `d` will only be updated once with the | ||
changed values of `b` and `c`. This avoids superfluous updates of your streams. | ||
twiggers `c` which _again_ triggers `d`. | ||
But Flyd handles such cases optimally. Since only one value entered the | ||
system `d` will only be updated once with the changed values of `b` and `c`. | ||
Flyd gurantees that when a single value enters the system every stream will | ||
only be updated once and with all it's dependencies in their most recent state. | ||
This avoids superfluous updates of your streams and intermediate states when | ||
several stream change at the same time. | ||
### Environment support | ||
@@ -534,0 +657,0 @@ |
@@ -204,3 +204,4 @@ var assert = require('assert'); | ||
s.end(true); | ||
assert(s.ended); | ||
assert(s.end()); | ||
assert(s.end()); | ||
}); | ||
@@ -218,3 +219,3 @@ it('detaches it from dependencies', function() { | ||
assert.equal(x.listeners.length, 0); | ||
assert(sum.ended); | ||
assert(sum.end()); | ||
}); | ||
@@ -231,7 +232,7 @@ it('ends its dependents', function() { | ||
x.end(true); | ||
assert(x.ended); | ||
assert(x.end()); | ||
assert.equal(x.listeners.length, 0); | ||
assert(y.ended); | ||
assert(y.end()); | ||
assert.equal(y.listeners.length, 0); | ||
assert(z.ended); | ||
assert(z.end()); | ||
}); | ||
@@ -249,9 +250,9 @@ it('updates children if stream ends after recieving value', function() { | ||
assert.equal(y(), z()); | ||
assert(!y.ended); | ||
assert(!z.ended); | ||
assert(!y.end()); | ||
assert(!z.end()); | ||
x(0); | ||
assert.equal(x.listeners.length, 1); | ||
assert(y.ended); | ||
assert(y.end()); | ||
assert.equal(y.listeners.length, 0); | ||
assert(z.ended); | ||
assert(z.end()); | ||
assert.equal(2, y()); | ||
@@ -267,4 +268,26 @@ assert.equal(2, z()); | ||
x(2); | ||
assert.equal(undefined, y.end()); | ||
assert.equal(2 * x(), y()); | ||
}); | ||
it('end stream does not have value even if base stream has initial value', function() { | ||
var killer = stream(true); | ||
var x = stream(1); | ||
var y = flyd.endsOn(killer, stream([x], function(self) { | ||
return 2 * x(); | ||
})); | ||
assert.equal(false, y.end.hasVal); | ||
}); | ||
it('ends stream can be changed without affecting listeners', function() { | ||
var killer1 = stream(); | ||
var killer2 = stream(); | ||
var ended = false; | ||
var x = stream(1); | ||
var y = flyd.endsOn(killer1, stream([x], function(self) { | ||
return 2 * x(); | ||
})); | ||
flyd.map(function() { ended = true; }, y.end); | ||
flyd.endsOn(killer2, y); | ||
killer2(true); | ||
assert(ended); | ||
}); | ||
}); | ||
@@ -392,6 +415,6 @@ describe('promise integration', function() { | ||
s1.end(true); | ||
assert(!s1and2.ended); | ||
assert(!s1and2.end()); | ||
s2(12)(2); | ||
s2.end(true); | ||
assert(s1and2.ended); | ||
assert(s1and2.end()); | ||
assert.deepEqual(result, [12, 2, 4, 44, 1, 12, 2]); | ||
@@ -518,2 +541,18 @@ }); | ||
}); | ||
it('handles reduced stream and ends', function() { | ||
var result = []; | ||
var s1 = stream(); | ||
var tx = t.compose( | ||
t.map(function(x) { return x * 2; }), | ||
t.take(3) | ||
); | ||
var s2 = flyd.transduce(tx, s1); | ||
stream([s2], function() { result.push(s2()); }); | ||
s1(1)(2); | ||
assert.notEqual(true, s2.end()); | ||
s1(3); | ||
assert.equal(true, s2.end()); | ||
s1(4); | ||
assert.deepEqual(result, [2, 4, 6]); | ||
}); | ||
}); | ||
@@ -520,0 +559,0 @@ describe('Ramda transducer support', function() { |
108066
1981
644