diff --git a/package.json b/package.json
index 749630d4..a74c7235 100644
--- a/package.json
+++ b/package.json
@@ -39,15 +39,13 @@
"semi": false,
"singleQuote": true
},
- "workspaces": {
- "packages": [
- "packages/microcosm",
- "packages/microcosm-devtools",
- "packages/microcosm-dom",
- "packages/microcosm-graphql",
- "packages/microcosm-http",
- "packages/microcosm-www",
- "packages/examples/*"
- ]
- }
+ "workspaces": [
+ "packages/microcosm",
+ "packages/microcosm-devtools",
+ "packages/microcosm-dom",
+ "packages/microcosm-graphql",
+ "packages/microcosm-http",
+ "packages/microcosm-www",
+ "packages/examples/*"
+ ]
}
diff --git a/packages/examples/file-uploads/.gitignore b/packages/examples/file-uploads/.gitignore
new file mode 100644
index 00000000..d45e6de3
--- /dev/null
+++ b/packages/examples/file-uploads/.gitignore
@@ -0,0 +1 @@
+public/uploads/*
diff --git a/packages/examples/file-uploads/package.json b/packages/examples/file-uploads/package.json
new file mode 100644
index 00000000..6605bf52
--- /dev/null
+++ b/packages/examples/file-uploads/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "examples-file-uploads",
+ "version": "0.0.0",
+ "private": true,
+ "license": "MIT",
+ "scripts": {
+ "start": "webpack-dev-server --mode=development"
+ },
+ "dependencies": {
+ "babel-loader": "^7.1.2",
+ "body-parser": "^1.18.2",
+ "html-webpack-plugin": "^3.0.6",
+ "multer": "^1.3.0",
+ "react": "^16.3.2",
+ "react-dom": "^16.3.2",
+ "webpack": "^4.6.0",
+ "webpack-dev-server": "^3.1.3"
+ }
+}
diff --git a/packages/microcosm-http/example/index.html b/packages/examples/file-uploads/public/index.html
similarity index 100%
rename from packages/microcosm-http/example/index.html
rename to packages/examples/file-uploads/public/index.html
diff --git a/packages/examples/file-uploads/src/actions/files.js b/packages/examples/file-uploads/src/actions/files.js
new file mode 100644
index 00000000..87feaf3d
--- /dev/null
+++ b/packages/examples/file-uploads/src/actions/files.js
@@ -0,0 +1,3 @@
+import http from 'microcosm-http'
+
+export const uploadFile = http.prepare({ method: 'POST', url: '/files' })
diff --git a/packages/examples/file-uploads/src/index.js b/packages/examples/file-uploads/src/index.js
new file mode 100644
index 00000000..881f30d0
--- /dev/null
+++ b/packages/examples/file-uploads/src/index.js
@@ -0,0 +1,5 @@
+import React from 'react'
+import DOM from 'react-dom'
+import { FileUploader } from './views/file-uploader'
+
+DOM.render(, document.getElementById('app'))
diff --git a/packages/examples/file-uploads/src/views/file-uploader.js b/packages/examples/file-uploads/src/views/file-uploader.js
new file mode 100644
index 00000000..60788530
--- /dev/null
+++ b/packages/examples/file-uploads/src/views/file-uploader.js
@@ -0,0 +1,46 @@
+import React from 'react'
+import { uploadFile } from '../actions/files'
+import { Subject } from 'microcosm'
+import { Presenter, ActionForm } from 'microcosm-dom'
+import { Progress } from './progress'
+
+function asFormData(form) {
+ return { data: new FormData(form) }
+}
+
+export class FileUploader extends Presenter {
+ state = {
+ status: 'inactive',
+ file: undefined
+ }
+
+ queue = new Subject()
+
+ render() {
+ let { status, file } = this.state
+
+ return (
+
+
+
+
+
+
+
+
+ )
+ }
+
+ trackProgress = action => {
+ action.every(iteration =>
+ this.setState({ status: iteration.status, file: iteration.payload })
+ )
+ }
+}
diff --git a/packages/examples/file-uploads/src/views/progress.js b/packages/examples/file-uploads/src/views/progress.js
new file mode 100644
index 00000000..4570e842
--- /dev/null
+++ b/packages/examples/file-uploads/src/views/progress.js
@@ -0,0 +1,28 @@
+import React from 'react'
+
+function Loading({ file, onCancel }) {
+ return (
+
+
Your files are uploading...
+
+
+
+ )
+}
+
+export function Progress({ status, file, onCancel }) {
+ switch (status) {
+ case 'next':
+ return
+ case 'error':
+ return {file.message}
+ case 'complete':
+ return Files sent!
+ case 'cancel':
+ return File upload cancelled
+ default:
+ return null
+ }
+}
diff --git a/packages/examples/file-uploads/webpack.config.js b/packages/examples/file-uploads/webpack.config.js
new file mode 100644
index 00000000..36bd2614
--- /dev/null
+++ b/packages/examples/file-uploads/webpack.config.js
@@ -0,0 +1,43 @@
+const HTMLWebpackPlugin = require('html-webpack-plugin')
+const path = require('path')
+const multer = require('multer')
+const bodyParser = require('body-parser')
+
+module.exports = {
+ plugins: [
+ new HTMLWebpackPlugin({
+ template: 'public/index.html'
+ })
+ ],
+ resolve: {
+ alias: {
+ microcosm: path.resolve(__dirname, '../../microcosm/src/'),
+ 'microcosm-http': path.resolve(
+ __dirname,
+ '../../microcosm-http/src/http.js'
+ ),
+ 'microcosm-dom': path.resolve(__dirname, '../../microcosm-dom/src/react')
+ }
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js/,
+ loader: 'babel-loader',
+ exclude: /node_modules/
+ }
+ ]
+ },
+ devServer: {
+ before: app => {
+ const upload = multer({ dest: `${__dirname}/public/uploads` })
+
+ app.use(bodyParser.json())
+ app.use(bodyParser.urlencoded({ extended: true }))
+
+ app.post('/files', upload.array('files', 100), function(req, res, next) {
+ res.json(req.body)
+ })
+ }
+ }
+}
diff --git a/packages/microcosm-dom/src/action-button.js b/packages/microcosm-dom/src/action-button.js
index 85793b65..37d466bd 100644
--- a/packages/microcosm-dom/src/action-button.js
+++ b/packages/microcosm-dom/src/action-button.js
@@ -6,7 +6,7 @@ export function generateActionButton(createElement, Component) {
constructor() {
super(...arguments)
- this.queue = new Subject('action-button')
+ this.queue = this.props.queue || new Subject()
this._onClick = this._onClick.bind(this)
}
@@ -15,7 +15,7 @@ export function generateActionButton(createElement, Component) {
}
componentWillUnmount() {
- this.queue.cancel()
+ this.queue.complete()
}
render() {
@@ -24,47 +24,45 @@ export function generateActionButton(createElement, Component) {
delete props.tag
delete props.action
delete props.value
- delete props.onStart
+ delete props.onSend
delete props.onNext
delete props.onComplete
+ delete props.onChange
delete props.onError
delete props.onCancel
delete props.send
delete props.prepare
- if (this.props.tag === 'button' && props.type == null) {
- props.type = 'button'
- }
-
return createElement(this.props.tag, props)
}
click() {
- let { action, prepare, value } = this.props
-
- let params = prepare(value)
- let result = this.send(action, params)
+ let result = this.send(this.props.action, this._parameterize())
+ let action = Subject.hash(result)
- if (result && 'subscribe' in result) {
- this._onChange('start', result)
+ this.props.onSend(action)
- result.subscribe({
- error: this._onChange.bind(this, 'error', result),
- next: this._onChange.bind(this, 'next', result),
- complete: this._onChange.bind(this, 'complete', result),
- cancel: this._onChange.bind(this, 'cancel', result)
- })
+ let tracker = action.every(this._onChange, this)
- this.queue.subscribe(result)
- }
+ this.queue.subscribe({
+ error: tracker.unsubscribe,
+ complete: tracker.unsubscribe,
+ cancel: action.cancel
+ })
- return result
+ return action
}
// Private --------------------------------------------------- //
- _onChange(status, result) {
- this.props[toCallbackName(status)](result.payload, result.meta)
+ _parameterize() {
+ let { value, prepare } = this.props
+
+ return prepare(value)
+ }
+
+ _onChange(action) {
+ this.props[toCallbackName(action.status)](action)
}
_onClick(event) {
@@ -80,12 +78,14 @@ export function generateActionButton(createElement, Component) {
ActionButton.defaultProps = {
action: 'no-action',
onClick: identity,
- onStart: identity,
+ onSend: identity,
onNext: identity,
onComplete: identity,
onError: identity,
+ onChange: identity,
onCancel: identity,
prepare: identity,
+ queue: null,
send: null,
tag: 'button',
value: null
diff --git a/packages/microcosm-dom/src/action-form.js b/packages/microcosm-dom/src/action-form.js
index c3907be9..08b31a2e 100644
--- a/packages/microcosm-dom/src/action-form.js
+++ b/packages/microcosm-dom/src/action-form.js
@@ -7,7 +7,7 @@ export function generateActionForm(createElement, Component) {
constructor() {
super(...arguments)
- this.queue = new Subject('action-form')
+ this.queue = this.props.queue || new Subject()
this._onSubmit = this._onSubmit.bind(this)
}
@@ -16,7 +16,7 @@ export function generateActionForm(createElement, Component) {
}
componentWillUnmount() {
- this.queue.cancel()
+ this.queue.complete()
}
render() {
@@ -29,7 +29,7 @@ export function generateActionForm(createElement, Component) {
delete props.action
delete props.prepare
delete props.serializer
- delete props.onStart
+ delete props.onSend
delete props.onNext
delete props.onComplete
delete props.onNext
@@ -43,21 +43,19 @@ export function generateActionForm(createElement, Component) {
submit(event) {
let result = this.send(this.props.action, this._parameterize())
+ let action = Subject.hash(result)
- if (result && 'subscribe' in result) {
- this._onChange('start', result)
+ this.props.onSend(action)
- result.subscribe({
- error: this._onChange.bind(this, 'error', result),
- next: this._onChange.bind(this, 'next', result),
- complete: this._onChange.bind(this, 'complete', result),
- cancel: this._onChange.bind(this, 'cancel', result)
- })
+ let tracker = action.every(this._onChange, this)
- this.queue.subscribe(result)
- }
+ this.queue.subscribe({
+ error: tracker.unsubscribe,
+ complete: tracker.unsubscribe,
+ cancel: action.cancel
+ })
- return result
+ return action
}
// Private --------------------------------------------------- //
@@ -70,8 +68,8 @@ export function generateActionForm(createElement, Component) {
: prepare(serializer(this._form))
}
- _onChange(status, result) {
- this.props[toCallbackName(status)](result.payload, result.meta)
+ _onChange(action) {
+ this.props[toCallbackName(action.status)](action)
}
_onSubmit(event) {
@@ -87,12 +85,14 @@ export function generateActionForm(createElement, Component) {
ActionForm.defaultProps = {
action: 'no-action',
onSubmit: identity,
- onStart: identity,
+ onSend: identity,
onNext: identity,
onComplete: identity,
+ onChange: identity,
onError: identity,
onCancel: identity,
prepare: identity,
+ queue: null,
send: null,
tag: 'form',
serializer: form => serialize(form, { hash: true, empty: true })
diff --git a/packages/microcosm-dom/src/presenter.js b/packages/microcosm-dom/src/presenter.js
index df0d80f0..9d632d8c 100644
--- a/packages/microcosm-dom/src/presenter.js
+++ b/packages/microcosm-dom/src/presenter.js
@@ -1,4 +1,4 @@
-import { Microcosm, Observable, Subject } from 'microcosm'
+import { Microcosm, Subject } from 'microcosm'
import { advice, noop, shallowDiffers } from './utilities'
import { intercept } from './intercept'
@@ -82,7 +82,6 @@ export function generatePresenter(createElement, Component) {
}
componentWillUnmount() {
- this.mediator.model.cancel()
this.teardown(this.repo, this.props, this.state)
if (this.didFork) {
@@ -107,7 +106,7 @@ export function generatePresenter(createElement, Component) {
constructor(props, context) {
super(props, context)
- this.model = Observable.of({})
+ this.model = new Subject({})
this.presenter = props.presenter
let prepo = props.repo || context.repo || new Microcosm()
@@ -137,6 +136,10 @@ export function generatePresenter(createElement, Component) {
this.updateModel(this.presenter.props, this.presenter.state)
}
+ componentWillUnmount() {
+ this.model.cancel()
+ }
+
render() {
return Object.getPrototypeOf(this.presenter).render.call(this.presenter)
}
diff --git a/packages/microcosm-dom/test/action-button.test.js b/packages/microcosm-dom/test/action-button.test.js
index 2cd9eff5..13beb466 100644
--- a/packages/microcosm-dom/test/action-button.test.js
+++ b/packages/microcosm-dom/test/action-button.test.js
@@ -13,18 +13,18 @@ describe('ActionButton', function() {
expect(send).toHaveBeenCalledWith('test', true)
})
- it('executes onStart when that action starts', function() {
+ it('executes onSend when that action starts', function() {
let repo = new Microcosm()
let send = action => repo.push(action, true)
- let onStart = jest.fn()
+ let onSend = jest.fn()
let button = mount(
-
+
)
button.click()
- expect(onStart).toHaveBeenCalledWith(true, repo.history.head.meta)
+ expect(onSend).toHaveBeenCalledWith(repo.history.head)
})
it('executes onComplete when that action completes', function() {
@@ -36,7 +36,7 @@ describe('ActionButton', function() {
).click()
- expect(onComplete).toHaveBeenCalledWith(true, repo.history.head.meta)
+ expect(onComplete).toHaveBeenCalledWith(repo.history.head)
})
it('executes onError when that action completes', function() {
@@ -48,7 +48,7 @@ describe('ActionButton', function() {
button.click()
- expect(onError).toHaveBeenCalledWith('bad', repo.history.head.meta)
+ expect(onError).toHaveBeenCalledWith(repo.history.head)
})
it('executes onNext when that action sends an update', function() {
@@ -62,7 +62,7 @@ describe('ActionButton', function() {
action.next('loading')
- expect(onNext).toHaveBeenCalledWith('loading', repo.history.head.meta)
+ expect(onNext).toHaveBeenCalledWith(repo.history.head)
})
it('executes onCancel when that action is cancelled', function() {
@@ -74,7 +74,7 @@ describe('ActionButton', function() {
button.click()
- expect(onCancel).toHaveBeenCalledWith(undefined, repo.history.head.meta)
+ expect(onCancel).toHaveBeenCalledWith(repo.history.head)
})
it('passes along onClick', function() {
@@ -154,18 +154,6 @@ describe('ActionButton', function() {
expect(wrapper.tagName).toBe('A')
})
- it('uses the button type when set as a button', function() {
- let wrapper = mount()
-
- expect(wrapper.type).toBe('button')
- })
-
- it('does not pass the type attribute for non-buttons', function() {
- let wrapper = mount()
-
- expect(wrapper.getAttribute('type')).toBe(null)
- })
-
it('inherits send from context', function() {
let send = jest.fn()
diff --git a/packages/microcosm-dom/test/action-form.test.js b/packages/microcosm-dom/test/action-form.test.js
index 09499d49..05c3a90b 100644
--- a/packages/microcosm-dom/test/action-form.test.js
+++ b/packages/microcosm-dom/test/action-form.test.js
@@ -16,7 +16,7 @@ describe('ActionForm', function() {
submit(form)
- expect(onComplete).toHaveBeenCalledWith(true, repo.history.head.meta)
+ expect(onComplete).toHaveBeenCalledWith(repo.history.head)
})
it('executes onError when that action fails', function() {
@@ -27,24 +27,24 @@ describe('ActionForm', function() {
submit(form)
- expect(onError).toHaveBeenCalledWith('bad', repo.history.head.meta)
+ expect(onError).toHaveBeenCalledWith(repo.history.head)
})
- it('executes onStart when that action opens', function() {
+ it('executes onSend when that action opens', function() {
let repo = new Microcosm()
let reply = () => repo.push(() => action => action.next('open'))
- let onStart = jest.fn()
+ let onSend = jest.fn()
let form = mount(
-
+
)
submit(form)
- expect(onStart).toHaveBeenCalledWith('open', repo.history.head.meta)
+ expect(onSend).toHaveBeenCalledWith(repo.history.head)
})
- it('executes onUpdate when that action sends an update', function() {
+ it('executes onNext when that action sends an update', function() {
let repo = new Microcosm()
let action = repo.push(() => action => {})
let reply = () => action
@@ -58,17 +58,17 @@ describe('ActionForm', function() {
action.next('loading')
- expect(onNext).toHaveBeenCalledWith('loading', repo.history.head.meta)
+ expect(onNext).toHaveBeenCalledWith(repo.history.head)
})
- it('does not execute callbacks if not given an action', function() {
+ it('gracefully executes callbacks if not given an action', function() {
let onComplete = jest.fn()
let form = mount( null} />)
submit(form)
- expect(onComplete).not.toHaveBeenCalled()
+ expect(onComplete).toHaveBeenCalled()
})
it('removes action callbacks when the component unmounts', function() {
@@ -105,7 +105,7 @@ describe('ActionForm', function() {
)
)
- expect(onComplete).toHaveBeenCalledWith({}, repo.history.head.meta)
+ expect(onComplete).toHaveBeenCalled()
})
it('send as a prop overrides context', function() {
@@ -121,10 +121,7 @@ describe('ActionForm', function() {
)
)
- expect(onComplete).toHaveBeenCalledWith(
- 'from-prop',
- repo.history.head.meta
- )
+ expect(onComplete).toHaveBeenCalled()
})
})
diff --git a/packages/microcosm-dom/test/presenter.test.js b/packages/microcosm-dom/test/presenter.test.js
index 63e7f9c9..1abc6d9c 100644
--- a/packages/microcosm-dom/test/presenter.test.js
+++ b/packages/microcosm-dom/test/presenter.test.js
@@ -131,7 +131,7 @@ describe('::getModel', function() {
expect(el.textContent).toEqual('Captain Kurtz')
})
- it('does not recalculate the view model if the props are the same', function() {
+ it('does not recalculate when changes during teardown occur', function() {
let spy = jest.fn()
class Namer extends Presenter {
@@ -540,7 +540,7 @@ describe('::teardown', function() {
expect(renders).toBe(1)
})
- it('changes during teardown do not cause a recalculation', function() {
+ it('changes during teardown do not cause a recalculation', async function() {
let renders = 0
class Test extends Presenter {
@@ -570,6 +570,8 @@ describe('::teardown', function() {
unmount(mount())
+ await delay()
+
// Once: for the initial calculation
expect(renders).toBe(1)
})
@@ -665,9 +667,7 @@ describe('Efficiency', function() {
let el = mount()
- repo.push(patch, { color: 'green' })
-
- await delay()
+ await repo.push(patch, { color: 'green' })
expect(model).toHaveBeenCalledTimes(1)
expect(el.textContent).toEqual('green')
diff --git a/packages/microcosm-http/example/index.js b/packages/microcosm-http/example/index.js
deleted file mode 100644
index d113a21e..00000000
--- a/packages/microcosm-http/example/index.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react'
-import DOM from 'react-dom'
-import Microcosm from 'microcosm'
-import ActionForm from 'microcosm/addons/action-form'
-import Presenter from 'microcosm/addons/presenter'
-import http from 'microcosm-http'
-
-let repo = new Microcosm()
-
-repo.addDomain('files', {
- actions: {
- uploadFile: http.prepare({ method: 'post', url: '/files' })
- }
-})
-
-class FileUploader extends Presenter {
- state = {
- status: 'inactive',
- payload: null
- }
-
- onNext = ({ status, payload }) => {
- this.setState({ status, payload })
- }
-
- serialize(form) {
- return { data: new FormData(form) }
- }
-
- render() {
- const { status, payload } = this.state
-
- if (status === 'update') {
- return (
-
-
Your files are uploading...
-
-
- )
- }
-
- return (
-
- {status === 'reject' ? (
- {payload.message}
- ) : null}
-
- {status === 'resolve' ? (
- Files sent!
- ) : null}
-
-
-
-
-
-
- )
- }
-}
-
-DOM.render(, document.getElementById('app'))
diff --git a/packages/microcosm-http/jest.config.js b/packages/microcosm-http/jest.config.js
index 0fec06df..5b015a64 100644
--- a/packages/microcosm-http/jest.config.js
+++ b/packages/microcosm-http/jest.config.js
@@ -2,6 +2,5 @@ const { moduleNameMapper } = require('../../jest.config')
module.exports = {
modulePathIgnorePatterns: ['/build/'],
- collectCoverageFrom: ['src/**/*.js'],
moduleNameMapper: moduleNameMapper
}
diff --git a/packages/microcosm-http/package.json b/packages/microcosm-http/package.json
index 5745fb57..ad03973f 100644
--- a/packages/microcosm-http/package.json
+++ b/packages/microcosm-http/package.json
@@ -3,20 +3,11 @@
"version": "0.0.0",
"main": "src/http.js",
"private": true,
+ "scripts": {
+ "test": "jest"
+ },
"dependencies": {
"axios": "^0.18.0",
"microcosm": "^13.0.0-alpha"
- },
- "scripts": {
- "start": "webpack-dev-server"
- },
- "devDependencies": {
- "body-parser": "^1.18.1",
- "html-webpack-plugin": "^3.2.0",
- "multer": "^1.3.0",
- "react": "^16.3.2",
- "react-dom": "^16.3.2",
- "webpack": "^4.6.0",
- "webpack-dev-server": "^3.1.3"
}
}
diff --git a/packages/microcosm-http/src/http.js b/packages/microcosm-http/src/http.js
index 19c9484e..4ba87aea 100644
--- a/packages/microcosm-http/src/http.js
+++ b/packages/microcosm-http/src/http.js
@@ -44,7 +44,6 @@ function formatErrors(error) {
export default function http(args) {
return action => {
let source = CancelToken.source()
-
// https://github.com/mzabriskie/axios#request-config
let options = merge(args, {
cancelToken: source.token,
@@ -54,11 +53,14 @@ export default function http(args) {
axios(options)
.then(response => {
- action.next(response.data)
- action.complete()
+ action.complete(response.data)
})
.catch(error => {
- action.error(formatErrors(error))
+ if (axios.isCancel(error)) {
+ action.cancel()
+ } else {
+ action.error(formatErrors(error))
+ }
})
action.subscribe({ cancel: source.cancel })
diff --git a/packages/microcosm-http/test/test-helpers.js b/packages/microcosm-http/test/test-helpers.js
index e22460c2..69cb3a29 100644
--- a/packages/microcosm-http/test/test-helpers.js
+++ b/packages/microcosm-http/test/test-helpers.js
@@ -1,9 +1,7 @@
import settle from 'axios/lib/core/settle'
-import createError from 'axios/lib/core/createError'
const defaults = {
data: {},
- delay: 100,
status: 200,
uploads: [],
downloads: []
@@ -16,16 +14,6 @@ export function testAdapter(mock) {
let response = { ...options, config }
return new Promise(function(resolve, reject) {
- // Handle cancellation
- // https://github.com/axios/axios#cancellation
- if (config.cancelToken) {
- config.cancelToken.promise.then(reject)
- }
-
- if (response.status >= 400) {
- reject(createError('Mock request failure', config, 'MOCKFAILED', {}))
- }
-
// Emit a progress event for each item in a given array of
// downloads/uploads. This is an object matching the progress
// event api:
@@ -34,11 +22,13 @@ export function testAdapter(mock) {
options.uploads.forEach(config.onUploadProgress)
// Wait a bit to simulate asynchronous behavior.
- setTimeout(() => settle(resolve, reject, response), mock.delay)
+ let timer = setTimeout(() => settle(resolve, reject, response))
+
+ // Handle cancellation
+ // https://github.com/axios/axios#cancellation
+ if (config.cancelToken) {
+ config.cancelToken.promise.then(() => clearTimeout(timer))
+ }
})
}
}
-
-export function delay(time = 0) {
- return new Promise(resolve => setTimeout(resolve, time))
-}
diff --git a/packages/microcosm-http/test/unit/http.test.js b/packages/microcosm-http/test/unit/http.test.js
index 6413c8d4..f005b84c 100644
--- a/packages/microcosm-http/test/unit/http.test.js
+++ b/packages/microcosm-http/test/unit/http.test.js
@@ -1,6 +1,6 @@
-import Microcosm from 'microcosm'
+import { Microcosm } from 'microcosm'
import http from 'microcosm-http'
-import { delay, testAdapter } from '../test-helpers'
+import { testAdapter } from '../test-helpers'
it('fetches', async () => {
let repo = new Microcosm()
@@ -16,7 +16,7 @@ it('fetches', async () => {
expect(Array.isArray(posts)).toBe(true)
})
-it('cancels', async () => {
+it('cancels', () => {
let repo = new Microcosm()
let getPosts = http.prepare({
@@ -25,25 +25,28 @@ it('cancels', async () => {
let action = repo.push(getPosts)
- await delay(0)
-
- action.cancel('nevermind')
+ action.cancel()
expect(action).toHaveProperty('meta.status', 'cancel')
- expect(action).toHaveProperty('payload', 'nevermind')
})
it('errors', async () => {
+ expect.assertions(3)
+
let repo = new Microcosm()
- let getPosts = () =>
- http({
- adapter: testAdapter({ status: 400 })
- })
+ let getPosts = http.prepare({
+ adapter: testAdapter({ status: 400 })
+ })
let action = repo.push(getPosts)
- await delay(0)
+ try {
+ await action
+ } catch (error) {
+ expect(error.status).toEqual(400)
+ expect(error.message).toContain('Request failed with status code 400')
+ }
expect(action).toHaveProperty('meta.status', 'error')
})
diff --git a/packages/microcosm-http/webpack.config.js b/packages/microcosm-http/webpack.config.js
deleted file mode 100644
index 7da7db68..00000000
--- a/packages/microcosm-http/webpack.config.js
+++ /dev/null
@@ -1,50 +0,0 @@
-const path = require('path')
-const HTMLWebpackPlugin = require('html-webpack-plugin')
-
-module.exports = {
- entry: './example/index.js',
- output: {
- filename: 'microcosm-http.js',
- path: path.resolve('example/build')
- },
- plugins: [
- new HTMLWebpackPlugin({
- template: './example/index.html'
- })
- ],
- resolve: {
- alias: {
- 'microcosm-http': path.resolve(__dirname, 'src/http.js'),
- microcosm: path.resolve(__dirname, '../microcosm/src/')
- }
- },
- module: {
- loaders: [
- {
- test: /\.js/,
- loader: 'babel-loader',
- exclude: /node_modules/
- },
- {
- test: /\.json/,
- loader: 'json-loader'
- }
- ]
- },
- devServer: {
- port: 3000,
- contentBase: path.resolve(__dirname, 'example'),
- setup: app => {
- const multer = require('multer')
- const bodyParser = require('body-parser')
- const upload = multer({ dest: 'example/uploads' })
-
- app.use(bodyParser.json())
- app.use(bodyParser.urlencoded({ extended: true }))
-
- app.post('/files', upload.array('files', 100), function(req, res, next) {
- res.json(req.body)
- })
- }
- }
-}
diff --git a/packages/microcosm/src/history.js b/packages/microcosm/src/history.js
index dfad05b5..6aedc518 100644
--- a/packages/microcosm/src/history.js
+++ b/packages/microcosm/src/history.js
@@ -78,14 +78,7 @@ export class History extends Subject {
this.head = action
- let dispatch = this.dispatch.bind(this, action)
-
- action.subscribe({
- next: dispatch,
- error: dispatch,
- complete: dispatch,
- cancel: dispatch
- })
+ action.every(this.dispatch, this)
try {
coroutine(action, command, params, origin)
diff --git a/packages/microcosm/src/subject.js b/packages/microcosm/src/subject.js
index 0d29ef06..6322c3e3 100644
--- a/packages/microcosm/src/subject.js
+++ b/packages/microcosm/src/subject.js
@@ -95,7 +95,16 @@ export class Subject {
}
}
- subscribe(next: *) {
+ get clear(): * {
+ return () => {
+ this.payload = null
+ this.meta.status = 'start'
+
+ this._observers.forEach(observer => observer.cancel())
+ }
+ }
+
+ subscribe(next: *): * {
let observer = new Observer(...arguments)
if (this.closed) {
@@ -163,7 +172,22 @@ export class Subject {
})
}
+ every(fn: (subject: this) => void, scope: any): * {
+ let dispatch = fn.bind(scope, this)
+
+ return this.subscribe({
+ next: dispatch,
+ error: dispatch,
+ complete: dispatch,
+ cancel: dispatch
+ })
+ }
+
static hash(obj: *): Subject {
+ if (obj instanceof Subject) {
+ return obj
+ }
+
let subject = new Subject()
if (getObservable(obj)) {
diff --git a/packages/microcosm/test/unit/subject.test.js b/packages/microcosm/test/unit/subject.test.js
index f8b17fce..f14dfa04 100644
--- a/packages/microcosm/test/unit/subject.test.js
+++ b/packages/microcosm/test/unit/subject.test.js
@@ -168,6 +168,30 @@ describe('Subject', function() {
})
})
+ describe('clear', function() {
+ it('resets the subject', function() {
+ let subject = new Subject()
+
+ subject.next(true)
+ subject.clear()
+
+ expect(subject.status).toBe('start')
+ expect(subject.payload).toBe(null)
+ expect(subject.closed).toBe(false)
+ })
+
+ it('cancels observers', function() {
+ let subject = new Subject()
+ let cancel = jest.fn()
+
+ subject.subscribe({ cancel })
+
+ subject.clear()
+
+ expect(cancel).toHaveBeenCalledTimes(1)
+ })
+ })
+
describe('then', function() {
it('resolves when completed', async () => {
let subject = new Subject()
diff --git a/yarn.lock b/yarn.lock
index e0959976..9bf0358e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1403,7 +1403,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
-body-parser@1.18.2, body-parser@^1.18.0, body-parser@^1.18.1, body-parser@^1.18.2:
+body-parser@1.18.2, body-parser@^1.18.0, body-parser@^1.18.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
dependencies:
@@ -4548,7 +4548,7 @@ html-webpack-plugin@^2.30.1:
pretty-error "^2.0.2"
toposort "^1.0.0"
-html-webpack-plugin@^3.2.0:
+html-webpack-plugin@^3.0.6, html-webpack-plugin@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
dependencies: