Skip to content

Commit

Permalink
[added] MotionRuntime
Browse files Browse the repository at this point in the history
Summary: Closes #121

Reviewers: O3 Material JavaScript platform reviewers, O2 Material Motion, #material_motion, featherless

Reviewed By: O2 Material Motion, #material_motion, featherless

Subscribers: appsforartists

Tags: #material_motion

Differential Revision: http://codereview.cc/D2366
  • Loading branch information
appsforartists committed Dec 22, 2016
1 parent e9887a3 commit 3288749
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/streams/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"indefinite-observable": "0.3.0",
"tslib": "^1.2.0"
},
"devDependencies": {
"material-motion-testing-utils": "0.0.0"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com:material-motion/material-motion-js.git"
Expand Down
78 changes: 78 additions & 0 deletions packages/streams/src/MotionRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/** @license
* Copyright 2016 - present The Material Motion Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

import {
Subscription,
} from 'indefinite-observable';

import {
MotionObservable,
State,
} from './MotionObservable';

import {
MotionObserver,
ScopedWritable,
} from './types';

export type RuntimeWriteArgs<T> = {
stream: MotionObservable<T>,
to: ScopedWritable<T>,
};

/**
* A motion runtime writes streams to properties and observes the aggregate
* state.
*
* If any stream is active, the runtime is active. Otherwise, the runtime is at
* rest.
*/
export class MotionRuntime<T> {
_subscriptions: Set<Subscription> = new Set();
_activeObservers: Set<MotionObserver<any>> = new Set();

get aggregateState() {
return this._activeObservers.size === 0
? State.AT_REST
: State.ACTIVE;
}

/**
* Subscribes to the given stream and write its `next` values to the given
* property.
*/
write({ stream, to }: RuntimeWriteArgs<T>) {
const observer = {
next: (value: any) => {
to.write(value);
},

state: (state: State) => {
if (state === State.ACTIVE) {
this._activeObservers.add(observer);

} else {
this._activeObservers.delete(observer);
}
},
};

const subscription = stream.subscribe(observer);
this._subscriptions.add(subscription);
}
}

export default MotionRuntime;
195 changes: 195 additions & 0 deletions packages/streams/src/__tests__/MotionRuntime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/** @license
* Copyright 2016 - present The Material Motion Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

import { expect } from 'chai';

import {
beforeEach,
describe,
it,
} from 'mocha-sugar-free';

import {
stub,
} from 'sinon';

import {
MotionObservable,
MotionRuntime,
State,
} from '../';

import {
createMockObserver,
} from 'material-motion-testing-utils';

declare function require(name: string);

// chai really doesn't like being imported as an ES2015 module; will be fixed in v4
require('chai').use(
require('sinon-chai')
);

describe('MotionRuntime',
() => {
let runtime;
let stream;
let mockObserver;
let mockProperty;

beforeEach(
() => {
runtime = new MotionRuntime();

mockObserver = createMockObserver();
stream = new MotionObservable(mockObserver.connect);

mockProperty = {
write: stub(),
};
}
);

it(`should write values from the stream's next to the given property`,
() => {
runtime.write({
stream,
to: mockProperty,
});

mockObserver.next(1);
expect(mockProperty.write).to.have.been.calledWith(1);

mockObserver.next(2);
expect(mockProperty.write).to.have.been.calledWith(2);
}
);

it(`should start at rest`,
() => {
expect(runtime.aggregateState).to.equal(State.AT_REST);
}
);

it(`should become active when a stream does`,
() => {
runtime.write({
stream,
to: mockProperty,
});

mockObserver.state(State.ACTIVE);
expect(runtime.aggregateState).to.equal(State.ACTIVE);
}
);

it(`should be active if any streams are active`,
() => {
const mockObserver2 = createMockObserver();
const stream2 = new MotionObservable(mockObserver2.connect);

runtime.write({
stream,
to: mockProperty,
});

runtime.write({
stream: stream2,
to: mockProperty,
});

mockObserver.state(State.AT_REST);
mockObserver2.state(State.ACTIVE);

expect(runtime.aggregateState).to.equal(State.ACTIVE);
}
);

it(`should come to rest when all streams come to rest`,
() => {
const mockObserver2 = createMockObserver();
const stream2 = new MotionObservable(mockObserver2.connect);

runtime.write({
stream,
to: mockProperty,
});

runtime.write({
stream: stream2,
to: mockProperty,
});

mockObserver.state(State.ACTIVE);
mockObserver2.state(State.ACTIVE);
mockObserver.state(State.AT_REST);
mockObserver2.state(State.AT_REST);

expect(runtime.aggregateState).to.equal(State.AT_REST);
}
);

it(`should not be active unless a stream's state channel declares so`,
() => {
runtime.write({
stream,
to: mockProperty,
});

mockObserver.next(5);
expect(runtime.aggregateState).to.equal(State.AT_REST);
}
);

it(`should be accurate even if it receives imbalanced state streams`,
() => {
runtime.write({
stream,
to: mockProperty,
});

mockObserver.state(State.AT_REST);
mockObserver.state(State.AT_REST);
mockObserver.state(State.ACTIVE);
expect(runtime.aggregateState).to.equal(State.ACTIVE);

mockObserver.state(State.ACTIVE);
expect(runtime.aggregateState).to.equal(State.ACTIVE);

mockObserver.state(State.AT_REST);
expect(runtime.aggregateState).to.equal(State.AT_REST);
}
);

it(`should change between active and at rest as often as the underlying streams do`,
() => {
runtime.write({
stream,
to: mockProperty,
});

mockObserver.state(State.ACTIVE);
expect(runtime.aggregateState).to.equal(State.ACTIVE);

mockObserver.state(State.AT_REST);
expect(runtime.aggregateState).to.equal(State.AT_REST);

mockObserver.state(State.ACTIVE);
expect(runtime.aggregateState).to.equal(State.ACTIVE);
}
);
}
);
2 changes: 2 additions & 0 deletions packages/streams/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@
export * from './types';
export * from './MotionObservable';
export { default as MotionObservable } from './MotionObservable';
export * from './MotionRuntime';
export { default as MotionRuntime } from './MotionRuntime';

0 comments on commit 3288749

Please sign in to comment.